2024-01-01 10:31:45 +00:00
// Copyright (C) 2019 Nicola Murino
2022-07-17 18:16:00 +00:00
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, version 3.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
2023-01-03 09:18:30 +00:00
// along with this program. If not, see <https://www.gnu.org/licenses/>.
2022-07-17 18:16:00 +00:00
2019-10-07 16:19:01 +00:00
package httpd
2019-07-20 10:26:52 +00:00
import (
2021-11-13 12:25:43 +00:00
"bytes"
2020-01-31 22:26:56 +00:00
"context"
2021-02-01 18:04:15 +00:00
"errors"
2021-06-05 14:07:09 +00:00
"fmt"
2021-05-30 21:07:46 +00:00
"io"
2022-05-09 17:09:43 +00:00
"io/fs"
2021-06-05 14:07:09 +00:00
"mime"
2019-07-20 10:26:52 +00:00
"net/http"
2021-10-23 13:47:21 +00:00
"net/url"
2019-12-27 22:12:44 +00:00
"os"
2021-05-30 21:07:46 +00:00
"path"
2021-02-01 18:04:15 +00:00
"strconv"
2021-05-30 21:07:46 +00:00
"strings"
2022-09-25 17:48:55 +00:00
"sync"
2021-06-05 14:07:09 +00:00
"time"
2019-07-20 10:26:52 +00:00
2021-10-23 13:47:21 +00:00
"github.com/go-chi/chi/v5"
2021-11-13 12:25:43 +00:00
"github.com/go-chi/chi/v5/middleware"
2020-05-06 17:36:34 +00:00
"github.com/go-chi/render"
2021-05-30 21:07:46 +00:00
"github.com/klauspost/compress/zip"
2023-05-12 16:34:59 +00:00
"github.com/sftpgo/sdk/plugin/notifier"
2020-05-06 17:36:34 +00:00
2022-07-24 14:18:54 +00:00
"github.com/drakkan/sftpgo/v2/internal/common"
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
"github.com/drakkan/sftpgo/v2/internal/logger"
"github.com/drakkan/sftpgo/v2/internal/metric"
"github.com/drakkan/sftpgo/v2/internal/plugin"
"github.com/drakkan/sftpgo/v2/internal/smtp"
"github.com/drakkan/sftpgo/v2/internal/util"
2019-07-20 10:26:52 +00:00
)
2021-06-05 14:07:09 +00:00
type pwdChange struct {
CurrentPassword string ` json:"current_password" `
NewPassword string ` json:"new_password" `
}
2021-11-13 12:25:43 +00:00
type pwdReset struct {
Code string ` json:"code" `
Password string ` json:"password" `
}
2021-09-29 16:46:15 +00:00
type baseProfile struct {
Email string ` json:"email,omitempty" `
Description string ` json:"description,omitempty" `
AllowAPIKeyAuth bool ` json:"allow_api_key_auth" `
}
type adminProfile struct {
baseProfile
}
type userProfile struct {
baseProfile
PublicKeys [ ] string ` json:"public_keys,omitempty" `
2021-09-06 16:46:35 +00:00
}
2019-10-07 16:19:01 +00:00
func sendAPIResponse ( w http . ResponseWriter , r * http . Request , err error , message string , code int ) {
var errorString string
2023-01-25 17:49:03 +00:00
if errors . Is ( err , util . ErrNotFound ) {
2021-08-17 16:08:32 +00:00
errorString = http . StatusText ( http . StatusNotFound )
} else if err != nil {
2019-10-07 16:19:01 +00:00
errorString = err . Error ( )
}
resp := apiResponse {
2020-09-08 07:45:21 +00:00
Error : errorString ,
Message : message ,
2019-10-07 16:19:01 +00:00
}
2020-01-31 22:26:56 +00:00
ctx := context . WithValue ( r . Context ( ) , render . StatusCtxKey , code )
render . JSON ( w , r . WithContext ( ctx ) , resp )
2019-10-07 16:19:01 +00:00
}
func getRespStatus ( err error ) int {
2023-01-21 14:41:24 +00:00
if errors . Is ( err , util . ErrValidation ) {
2019-10-07 16:19:01 +00:00
return http . StatusBadRequest
}
2023-01-21 14:41:24 +00:00
if errors . Is ( err , util . ErrMethodDisabled ) {
2019-10-07 16:19:01 +00:00
return http . StatusForbidden
}
2023-01-21 14:41:24 +00:00
if errors . Is ( err , util . ErrNotFound ) {
2020-06-20 10:38:04 +00:00
return http . StatusNotFound
}
2022-05-09 17:09:43 +00:00
if errors . Is ( err , fs . ErrNotExist ) {
2019-12-27 22:12:44 +00:00
return http . StatusBadRequest
}
2022-05-09 17:09:43 +00:00
if errors . Is ( err , fs . ErrPermission ) || errors . Is ( err , dataprovider . ErrLoginNotAllowedFromIP ) {
2021-10-09 12:17:28 +00:00
return http . StatusForbidden
}
2021-12-25 11:08:07 +00:00
if errors . Is ( err , plugin . ErrNoSearcher ) || errors . Is ( err , dataprovider . ErrNotImplemented ) {
2021-10-23 13:47:21 +00:00
return http . StatusNotImplemented
}
2024-01-17 16:36:35 +00:00
if errors . Is ( err , dataprovider . ErrDuplicatedKey ) || errors . Is ( err , dataprovider . ErrForeignKeyViolated ) {
return http . StatusConflict
}
2019-10-07 16:19:01 +00:00
return http . StatusInternalServerError
}
2021-12-19 11:14:53 +00:00
// mappig between fs errors for HTTP protocol and HTTP response status codes
2021-06-05 14:07:09 +00:00
func getMappedStatusCode ( err error ) int {
var statusCode int
2021-12-19 11:14:53 +00:00
switch {
2023-12-28 17:43:07 +00:00
case errors . Is ( err , fs . ErrPermission ) :
2021-06-05 14:07:09 +00:00
statusCode = http . StatusForbidden
2022-01-30 10:42:36 +00:00
case errors . Is ( err , common . ErrReadQuotaExceeded ) :
statusCode = http . StatusForbidden
2023-12-28 17:43:07 +00:00
case errors . Is ( err , fs . ErrNotExist ) :
2021-06-05 14:07:09 +00:00
statusCode = http . StatusNotFound
2021-12-19 11:14:53 +00:00
case errors . Is ( err , common . ErrQuotaExceeded ) :
2021-11-22 11:25:36 +00:00
statusCode = http . StatusRequestEntityTooLarge
2021-12-28 11:03:52 +00:00
case errors . Is ( err , common . ErrOpUnsupported ) :
statusCode = http . StatusBadRequest
2021-06-05 14:07:09 +00:00
default :
2023-02-27 10:03:05 +00:00
if _ , ok := err . ( * http . MaxBytesError ) ; ok {
statusCode = http . StatusRequestEntityTooLarge
} else {
statusCode = http . StatusInternalServerError
}
2021-06-05 14:07:09 +00:00
}
return statusCode
}
2021-10-23 13:47:21 +00:00
func getURLParam ( r * http . Request , key string ) string {
v := chi . URLParam ( r , key )
unescaped , err := url . PathUnescape ( v )
if err != nil {
return v
}
return unescaped
}
func getCommaSeparatedQueryParam ( r * http . Request , key string ) [ ] string {
var result [ ] string
for _ , val := range strings . Split ( r . URL . Query ( ) . Get ( key ) , "," ) {
val = strings . TrimSpace ( val )
if val != "" {
result = append ( result , val )
}
}
2022-05-31 16:22:18 +00:00
return util . RemoveDuplicates ( result , false )
2021-10-23 13:47:21 +00:00
}
2021-12-19 11:14:53 +00:00
func getBoolQueryParam ( r * http . Request , param string ) bool {
return r . URL . Query ( ) . Get ( param ) == "true"
}
2022-09-25 17:48:55 +00:00
func getActiveConnections ( w http . ResponseWriter , r * http . Request ) {
r . Body = http . MaxBytesReader ( w , r . Body , maxRequestSize )
claims , err := getTokenClaims ( r )
if err != nil || claims . Username == "" {
sendAPIResponse ( w , r , err , "Invalid token claims" , http . StatusBadRequest )
return
}
2022-11-16 18:04:50 +00:00
stats := common . Connections . GetStats ( claims . Role )
2022-09-25 17:48:55 +00:00
if claims . NodeID == "" {
2022-11-16 18:04:50 +00:00
stats = append ( stats , getNodesConnections ( claims . Username , claims . Role ) ... )
2022-09-25 17:48:55 +00:00
}
render . JSON ( w , r , stats )
}
2021-01-17 21:29:08 +00:00
func handleCloseConnection ( w http . ResponseWriter , r * http . Request ) {
2021-08-17 16:08:32 +00:00
r . Body = http . MaxBytesReader ( w , r . Body , maxRequestSize )
2022-10-02 07:51:47 +00:00
claims , err := getTokenClaims ( r )
if err != nil || claims . Username == "" {
sendAPIResponse ( w , r , err , "Invalid token claims" , http . StatusBadRequest )
return
}
2021-01-17 21:29:08 +00:00
connectionID := getURLParam ( r , "connectionID" )
if connectionID == "" {
sendAPIResponse ( w , r , nil , "connectionID is mandatory" , http . StatusBadRequest )
return
2020-06-07 21:30:18 +00:00
}
2022-09-25 17:48:55 +00:00
node := r . URL . Query ( ) . Get ( "node" )
if node == "" || node == dataprovider . GetNodeName ( ) {
2022-11-16 18:04:50 +00:00
if common . Connections . Close ( connectionID , claims . Role ) {
2022-09-25 17:48:55 +00:00
sendAPIResponse ( w , r , nil , "Connection closed" , http . StatusOK )
} else {
sendAPIResponse ( w , r , nil , "Not Found" , http . StatusNotFound )
}
return
}
n , err := dataprovider . GetNodeByName ( node )
if err != nil {
logger . Warn ( logSender , "" , "unable to get node with name %q: %v" , node , err )
status := getRespStatus ( err )
sendAPIResponse ( w , r , nil , http . StatusText ( status ) , status )
return
}
2022-11-16 18:04:50 +00:00
if err := n . SendDeleteRequest ( claims . Username , claims . Role , fmt . Sprintf ( "%s/%s" , activeConnectionsPath , connectionID ) ) ; err != nil {
2022-09-25 17:48:55 +00:00
logger . Warn ( logSender , "" , "unable to delete connection id %q from node %q: %v" , connectionID , n . Name , err )
2021-01-17 21:29:08 +00:00
sendAPIResponse ( w , r , nil , "Not Found" , http . StatusNotFound )
2022-09-25 17:48:55 +00:00
return
2020-09-01 14:10:26 +00:00
}
2022-09-25 17:48:55 +00:00
sendAPIResponse ( w , r , nil , "Connection closed" , http . StatusOK )
}
// getNodesConnections returns the active connections from other nodes.
// Errors are silently ignored
2022-11-16 18:04:50 +00:00
func getNodesConnections ( admin , role string ) [ ] common . ConnectionStatus {
2022-09-25 17:48:55 +00:00
nodes , err := dataprovider . GetNodes ( )
if err != nil || len ( nodes ) == 0 {
return nil
}
var results [ ] common . ConnectionStatus
var mu sync . Mutex
var wg sync . WaitGroup
for _ , n := range nodes {
wg . Add ( 1 )
go func ( node dataprovider . Node ) {
defer wg . Done ( )
var stats [ ] common . ConnectionStatus
2022-11-16 18:04:50 +00:00
if err := node . SendGetRequest ( admin , role , activeConnectionsPath , & stats ) ; err != nil {
2022-09-25 17:48:55 +00:00
logger . Warn ( logSender , "" , "unable to get connections from node %s: %v" , node . Name , err )
return
}
mu . Lock ( )
results = append ( results , stats ... )
mu . Unlock ( )
} ( n )
}
wg . Wait ( )
return results
2020-09-01 14:10:26 +00:00
}
2021-02-01 18:04:15 +00:00
func getSearchFilters ( w http . ResponseWriter , r * http . Request ) ( int , int , string , error ) {
var err error
limit := 100
offset := 0
order := dataprovider . OrderASC
if _ , ok := r . URL . Query ( ) [ "limit" ] ; ok {
limit , err = strconv . Atoi ( r . URL . Query ( ) . Get ( "limit" ) )
if err != nil {
2021-03-21 18:15:47 +00:00
err = errors . New ( "invalid limit" )
2021-02-01 18:04:15 +00:00
sendAPIResponse ( w , r , err , "" , http . StatusBadRequest )
return limit , offset , order , err
}
if limit > 500 {
limit = 500
}
}
if _ , ok := r . URL . Query ( ) [ "offset" ] ; ok {
offset , err = strconv . Atoi ( r . URL . Query ( ) . Get ( "offset" ) )
if err != nil {
2021-03-21 18:15:47 +00:00
err = errors . New ( "invalid offset" )
2021-02-01 18:04:15 +00:00
sendAPIResponse ( w , r , err , "" , http . StatusBadRequest )
return limit , offset , order , err
}
}
if _ , ok := r . URL . Query ( ) [ "order" ] ; ok {
order = r . URL . Query ( ) . Get ( "order" )
if order != dataprovider . OrderASC && order != dataprovider . OrderDESC {
2021-03-21 18:15:47 +00:00
err = errors . New ( "invalid order" )
2021-02-01 18:04:15 +00:00
sendAPIResponse ( w , r , err , "" , http . StatusBadRequest )
return limit , offset , order , err
}
}
return limit , offset , order , err
}
2021-05-30 21:07:46 +00:00
2022-02-06 15:46:43 +00:00
func renderAPIDirContents ( w http . ResponseWriter , r * http . Request , contents [ ] os . FileInfo , omitNonRegularFiles bool ) {
2022-05-19 17:49:51 +00:00
results := make ( [ ] map [ string ] any , 0 , len ( contents ) )
2022-02-06 15:46:43 +00:00
for _ , info := range contents {
if omitNonRegularFiles && ! info . Mode ( ) . IsDir ( ) && ! info . Mode ( ) . IsRegular ( ) {
continue
}
2022-05-19 17:49:51 +00:00
res := make ( map [ string ] any )
2022-02-06 15:46:43 +00:00
res [ "name" ] = info . Name ( )
if info . Mode ( ) . IsRegular ( ) {
res [ "size" ] = info . Size ( )
}
res [ "mode" ] = info . Mode ( )
res [ "last_modified" ] = info . ModTime ( ) . UTC ( ) . Format ( time . RFC3339 )
results = append ( results , res )
}
render . JSON ( w , r , results )
}
2022-07-26 16:24:17 +00:00
func getCompressedFileName ( username string , files [ ] string ) string {
if len ( files ) == 1 {
name := path . Base ( files [ 0 ] )
return fmt . Sprintf ( "%s-%s.zip" , username , strings . TrimSuffix ( name , path . Ext ( name ) ) )
}
return fmt . Sprintf ( "%s-download.zip" , username )
}
2021-11-06 13:13:20 +00:00
func renderCompressedFiles ( w http . ResponseWriter , conn * Connection , baseDir string , files [ ] string ,
share * dataprovider . Share ,
) {
2022-09-07 14:23:56 +00:00
conn . User . CheckFsRoot ( conn . ID ) //nolint:errcheck
2021-05-30 21:07:46 +00:00
w . Header ( ) . Set ( "Content-Type" , "application/zip" )
w . Header ( ) . Set ( "Accept-Ranges" , "none" )
w . Header ( ) . Set ( "Content-Transfer-Encoding" , "binary" )
w . WriteHeader ( http . StatusOK )
wr := zip . NewWriter ( w )
for _ , file := range files {
2022-03-03 11:44:56 +00:00
fullPath := util . CleanPath ( path . Join ( baseDir , file ) )
2021-05-30 21:07:46 +00:00
if err := addZipEntry ( wr , conn , fullPath , baseDir ) ; err != nil {
2021-11-06 13:13:20 +00:00
if share != nil {
dataprovider . UpdateShareLastUse ( share , - 1 ) //nolint:errcheck
}
2021-05-30 21:07:46 +00:00
panic ( http . ErrAbortHandler )
}
}
if err := wr . Close ( ) ; err != nil {
2021-12-16 18:53:00 +00:00
conn . Log ( logger . LevelError , "unable to close zip file: %v" , err )
2021-11-06 13:13:20 +00:00
if share != nil {
dataprovider . UpdateShareLastUse ( share , - 1 ) //nolint:errcheck
}
2021-05-30 21:07:46 +00:00
panic ( http . ErrAbortHandler )
}
}
func addZipEntry ( wr * zip . Writer , conn * Connection , entryPath , baseDir string ) error {
info , err := conn . Stat ( entryPath , 1 )
if err != nil {
2023-02-27 18:02:43 +00:00
conn . Log ( logger . LevelDebug , "unable to add zip entry %q, stat error: %v" , entryPath , err )
2021-05-30 21:07:46 +00:00
return err
}
2022-09-19 15:06:42 +00:00
entryName , err := getZipEntryName ( entryPath , baseDir )
if err != nil {
conn . Log ( logger . LevelError , "unable to get zip entry name: %v" , err )
return err
}
2021-05-30 21:07:46 +00:00
if info . IsDir ( ) {
2022-09-19 15:06:42 +00:00
_ , err = wr . CreateHeader ( & zip . FileHeader {
Name : entryName + "/" ,
2022-05-09 17:09:43 +00:00
Method : zip . Deflate ,
Modified : info . ModTime ( ) ,
} )
2021-05-30 21:07:46 +00:00
if err != nil {
2023-02-27 18:02:43 +00:00
conn . Log ( logger . LevelError , "unable to create zip entry %q: %v" , entryPath , err )
2021-05-30 21:07:46 +00:00
return err
}
contents , err := conn . ReadDir ( entryPath )
if err != nil {
2023-02-27 18:02:43 +00:00
conn . Log ( logger . LevelDebug , "unable to add zip entry %q, read dir error: %v" , entryPath , err )
2021-05-30 21:07:46 +00:00
return err
}
for _ , info := range contents {
2022-03-03 11:44:56 +00:00
fullPath := util . CleanPath ( path . Join ( entryPath , info . Name ( ) ) )
2021-05-30 21:07:46 +00:00
if err := addZipEntry ( wr , conn , fullPath , baseDir ) ; err != nil {
return err
}
}
return nil
}
if ! info . Mode ( ) . IsRegular ( ) {
// we only allow regular files
2023-02-27 18:02:43 +00:00
conn . Log ( logger . LevelInfo , "skipping zip entry for non regular file %q" , entryPath )
2021-05-30 21:07:46 +00:00
return nil
}
reader , err := conn . getFileReader ( entryPath , 0 , http . MethodGet )
if err != nil {
2023-02-27 18:02:43 +00:00
conn . Log ( logger . LevelDebug , "unable to add zip entry %q, cannot open file: %v" , entryPath , err )
2021-05-30 21:07:46 +00:00
return err
}
defer reader . Close ( )
2022-05-09 17:09:43 +00:00
f , err := wr . CreateHeader ( & zip . FileHeader {
2022-09-19 15:06:42 +00:00
Name : entryName ,
2022-05-09 17:09:43 +00:00
Method : zip . Deflate ,
Modified : info . ModTime ( ) ,
} )
2021-05-30 21:07:46 +00:00
if err != nil {
2023-02-27 18:02:43 +00:00
conn . Log ( logger . LevelError , "unable to create zip entry %q: %v" , entryPath , err )
2021-05-30 21:07:46 +00:00
return err
}
_ , err = io . Copy ( f , reader )
return err
}
2022-09-19 15:06:42 +00:00
func getZipEntryName ( entryPath , baseDir string ) ( string , error ) {
if ! strings . HasPrefix ( entryPath , baseDir ) {
return "" , fmt . Errorf ( "entry path %q is outside base dir %q" , entryPath , baseDir )
}
2021-05-30 21:07:46 +00:00
entryPath = strings . TrimPrefix ( entryPath , baseDir )
2022-09-19 15:06:42 +00:00
return strings . TrimPrefix ( entryPath , "/" ) , nil
2021-05-30 21:07:46 +00:00
}
2021-06-05 14:07:09 +00:00
2022-02-06 15:46:43 +00:00
func checkDownloadFileFromShare ( share * dataprovider . Share , info os . FileInfo ) error {
if share != nil && ! info . Mode ( ) . IsRegular ( ) {
return util . NewValidationError ( "non regular files are not supported for shares" )
}
return nil
}
2021-11-25 18:24:32 +00:00
func downloadFile ( w http . ResponseWriter , r * http . Request , connection * Connection , name string ,
2022-02-06 15:46:43 +00:00
info os . FileInfo , inline bool , share * dataprovider . Share ,
2021-11-25 18:24:32 +00:00
) ( int , error ) {
2022-09-07 14:23:56 +00:00
connection . User . CheckFsRoot ( connection . ID ) //nolint:errcheck
2022-02-06 15:46:43 +00:00
err := checkDownloadFileFromShare ( share , info )
if err != nil {
return http . StatusBadRequest , err
}
2021-06-05 14:07:09 +00:00
rangeHeader := r . Header . Get ( "Range" )
if rangeHeader != "" && checkIfRange ( r , info . ModTime ( ) ) == condFalse {
rangeHeader = ""
}
offset := int64 ( 0 )
size := info . Size ( )
responseStatus := http . StatusOK
if strings . HasPrefix ( rangeHeader , "bytes=" ) {
if strings . Contains ( rangeHeader , "," ) {
2023-02-27 18:02:43 +00:00
return http . StatusRequestedRangeNotSatisfiable , fmt . Errorf ( "unsupported range %q" , rangeHeader )
2021-06-05 14:07:09 +00:00
}
offset , size , err = parseRangeRequest ( rangeHeader [ 6 : ] , size )
if err != nil {
return http . StatusRequestedRangeNotSatisfiable , err
}
responseStatus = http . StatusPartialContent
}
reader , err := connection . getFileReader ( name , offset , r . Method )
if err != nil {
2023-02-27 18:02:43 +00:00
return getMappedStatusCode ( err ) , fmt . Errorf ( "unable to read file %q: %v" , name , err )
2021-06-05 14:07:09 +00:00
}
defer reader . Close ( )
w . Header ( ) . Set ( "Last-Modified" , info . ModTime ( ) . UTC ( ) . Format ( http . TimeFormat ) )
if checkPreconditions ( w , r , info . ModTime ( ) ) {
return 0 , fmt . Errorf ( "%v" , http . StatusText ( http . StatusPreconditionFailed ) )
}
ctype := mime . TypeByExtension ( path . Ext ( name ) )
if ctype == "" {
ctype = "application/octet-stream"
}
if responseStatus == http . StatusPartialContent {
w . Header ( ) . Set ( "Content-Range" , fmt . Sprintf ( "bytes %d-%d/%d" , offset , offset + size - 1 , info . Size ( ) ) )
}
w . Header ( ) . Set ( "Content-Length" , strconv . FormatInt ( size , 10 ) )
w . Header ( ) . Set ( "Content-Type" , ctype )
2021-11-25 18:24:32 +00:00
if ! inline {
2023-02-27 18:02:43 +00:00
w . Header ( ) . Set ( "Content-Disposition" , fmt . Sprintf ( "attachment; filename=%q" , path . Base ( name ) ) )
2021-11-25 18:24:32 +00:00
}
2021-06-05 14:07:09 +00:00
w . Header ( ) . Set ( "Accept-Ranges" , "bytes" )
w . WriteHeader ( responseStatus )
if r . Method != http . MethodHead {
2022-01-30 10:42:36 +00:00
_ , err = io . CopyN ( w , reader , size )
if err != nil {
2022-02-06 15:46:43 +00:00
if share != nil {
dataprovider . UpdateShareLastUse ( share , - 1 ) //nolint:errcheck
}
2022-01-30 10:42:36 +00:00
connection . Log ( logger . LevelDebug , "error reading file to download: %v" , err )
panic ( http . ErrAbortHandler )
}
2021-06-05 14:07:09 +00:00
}
return http . StatusOK , nil
}
func checkPreconditions ( w http . ResponseWriter , r * http . Request , modtime time . Time ) bool {
if checkIfUnmodifiedSince ( r , modtime ) == condFalse {
w . WriteHeader ( http . StatusPreconditionFailed )
return true
}
if checkIfModifiedSince ( r , modtime ) == condFalse {
w . WriteHeader ( http . StatusNotModified )
return true
}
return false
}
func checkIfUnmodifiedSince ( r * http . Request , modtime time . Time ) condResult {
ius := r . Header . Get ( "If-Unmodified-Since" )
if ius == "" || isZeroTime ( modtime ) {
return condNone
}
t , err := http . ParseTime ( ius )
if err != nil {
return condNone
}
// The Last-Modified header truncates sub-second precision so
// the modtime needs to be truncated too.
modtime = modtime . Truncate ( time . Second )
if modtime . Before ( t ) || modtime . Equal ( t ) {
return condTrue
}
return condFalse
}
func checkIfModifiedSince ( r * http . Request , modtime time . Time ) condResult {
if r . Method != http . MethodGet && r . Method != http . MethodHead {
return condNone
}
ims := r . Header . Get ( "If-Modified-Since" )
if ims == "" || isZeroTime ( modtime ) {
return condNone
}
t , err := http . ParseTime ( ims )
if err != nil {
return condNone
}
// The Last-Modified header truncates sub-second precision so
// the modtime needs to be truncated too.
modtime = modtime . Truncate ( time . Second )
if modtime . Before ( t ) || modtime . Equal ( t ) {
return condFalse
}
return condTrue
}
func checkIfRange ( r * http . Request , modtime time . Time ) condResult {
if r . Method != http . MethodGet && r . Method != http . MethodHead {
return condNone
}
ir := r . Header . Get ( "If-Range" )
if ir == "" {
return condNone
}
if modtime . IsZero ( ) {
return condFalse
}
t , err := http . ParseTime ( ir )
if err != nil {
return condFalse
}
2022-07-21 16:42:22 +00:00
if modtime . Unix ( ) == t . Unix ( ) {
2021-06-05 14:07:09 +00:00
return condTrue
}
return condFalse
}
func parseRangeRequest ( bytesRange string , size int64 ) ( int64 , int64 , error ) {
var start , end int64
var err error
values := strings . Split ( bytesRange , "-" )
if values [ 0 ] == "" {
start = - 1
} else {
start , err = strconv . ParseInt ( values [ 0 ] , 10 , 64 )
if err != nil {
return start , size , err
}
}
if len ( values ) >= 2 {
if values [ 1 ] != "" {
end , err = strconv . ParseInt ( values [ 1 ] , 10 , 64 )
if err != nil {
return start , size , err
}
if end >= size {
end = size - 1
}
}
}
if start == - 1 && end == 0 {
2023-02-27 18:02:43 +00:00
return 0 , 0 , fmt . Errorf ( "unsupported range %q" , bytesRange )
2021-06-05 14:07:09 +00:00
}
if end > 0 {
if start == - 1 {
// we have something like -500
start = size - end
size = end
2022-07-21 16:42:22 +00:00
// start cannot be < 0 here, we did end = size -1 above
2021-06-05 14:07:09 +00:00
} else {
// we have something like 500-600
size = end - start + 1
if size < 0 {
2023-02-27 18:02:43 +00:00
return 0 , 0 , fmt . Errorf ( "unacceptable range %q" , bytesRange )
2021-06-05 14:07:09 +00:00
}
}
return start , size , nil
}
// we have something like 500-
size -= start
if size < 0 {
2023-02-27 18:02:43 +00:00
return 0 , 0 , fmt . Errorf ( "unacceptable range %q" , bytesRange )
2021-06-05 14:07:09 +00:00
}
return start , size , err
}
2023-03-04 12:55:48 +00:00
func handleDefenderEventLoginFailed ( ipAddr string , err error ) error {
event := common . HostEventLoginFailed
if errors . Is ( err , util . ErrNotFound ) {
event = common . HostEventUserNotFound
err = dataprovider . ErrInvalidCredentials
}
common . AddDefenderEvent ( ipAddr , common . ProtocolHTTP , event )
return err
}
2022-02-19 09:53:35 +00:00
func updateLoginMetrics ( user * dataprovider . User , loginMethod , ip string , err error ) {
metric . AddLoginAttempt ( loginMethod )
var protocol string
switch loginMethod {
case dataprovider . LoginMethodIDP :
protocol = common . ProtocolOIDC
default :
protocol = common . ProtocolHTTP
}
2021-09-11 16:23:11 +00:00
if err != nil && err != common . ErrInternalFailure && err != common . ErrNoCredentials {
2022-02-19 09:53:35 +00:00
logger . ConnectionFailedLog ( user . Username , ip , loginMethod , protocol , err . Error ( ) )
2023-03-04 12:55:48 +00:00
err = handleDefenderEventLoginFailed ( ip , err )
2023-05-12 16:34:59 +00:00
logEv := notifier . LogEventTypeLoginFailed
if errors . Is ( err , util . ErrNotFound ) {
logEv = notifier . LogEventTypeLoginNoUser
}
plugin . Handler . NotifyLogEvent ( logEv , protocol , user . Username , ip , "" , err )
2021-06-05 14:07:09 +00:00
}
2022-02-19 09:53:35 +00:00
metric . AddLoginResult ( loginMethod , err )
dataprovider . ExecutePostLoginHook ( user , loginMethod , ip , protocol , err )
2021-06-05 14:07:09 +00:00
}
2022-04-14 17:07:41 +00:00
func checkHTTPClientUser ( user * dataprovider . User , r * http . Request , connectionID string , checkSessions bool ) error {
2022-05-19 17:49:51 +00:00
if util . Contains ( user . Filters . DeniedProtocols , common . ProtocolHTTP ) {
2023-02-27 18:02:43 +00:00
logger . Info ( logSender , connectionID , "cannot login user %q, protocol HTTP is not allowed" , user . Username )
2023-12-10 15:40:13 +00:00
return util . NewI18nError (
fmt . Errorf ( "protocol HTTP is not allowed for user %q" , user . Username ) ,
util . I18nErrorProtocolForbidden ,
)
2021-06-05 14:07:09 +00:00
}
2023-08-04 18:56:23 +00:00
if ! isLoggedInWithOIDC ( r ) && ! user . IsLoginMethodAllowed ( dataprovider . LoginMethodPassword , common . ProtocolHTTP ) {
2023-02-27 18:02:43 +00:00
logger . Info ( logSender , connectionID , "cannot login user %q, password login method is not allowed" , user . Username )
2023-12-10 15:40:13 +00:00
return util . NewI18nError (
fmt . Errorf ( "login method password is not allowed for user %q" , user . Username ) ,
util . I18nErrorPwdLoginForbidden ,
)
2021-06-05 14:07:09 +00:00
}
2022-04-14 17:07:41 +00:00
if checkSessions && user . MaxSessions > 0 {
2021-06-05 14:07:09 +00:00
activeSessions := common . Connections . GetActiveSessions ( user . Username )
if activeSessions >= user . MaxSessions {
2023-02-27 18:02:43 +00:00
logger . Info ( logSender , connectionID , "authentication refused for user: %q, too many open sessions: %v/%v" , user . Username ,
2021-06-05 14:07:09 +00:00
activeSessions , user . MaxSessions )
2023-12-10 15:40:13 +00:00
return util . NewI18nError ( fmt . Errorf ( "too many open sessions: %v" , activeSessions ) , util . I18nError429Message )
2021-06-05 14:07:09 +00:00
}
}
if ! user . IsLoginFromAddrAllowed ( r . RemoteAddr ) {
2023-02-27 18:02:43 +00:00
logger . Info ( logSender , connectionID , "cannot login user %q, remote address is not allowed: %v" , user . Username , r . RemoteAddr )
2023-12-10 15:40:13 +00:00
return util . NewI18nError (
fmt . Errorf ( "login for user %q is not allowed from this address: %v" , user . Username , r . RemoteAddr ) ,
util . I18nErrorIPForbidden ,
)
2021-06-05 14:07:09 +00:00
}
return nil
}
2021-11-13 12:25:43 +00:00
func handleForgotPassword ( r * http . Request , username string , isAdmin bool ) error {
var email , subject string
var err error
var admin dataprovider . Admin
var user dataprovider . User
if username == "" {
2023-12-10 15:40:13 +00:00
return util . NewI18nError ( util . NewValidationError ( "username is mandatory" ) , util . I18nErrorUsernameRequired )
2021-11-13 12:25:43 +00:00
}
if isAdmin {
admin , err = dataprovider . AdminExists ( username )
email = admin . Email
2023-02-27 18:02:43 +00:00
subject = fmt . Sprintf ( "Email Verification Code for admin %q" , username )
2021-11-13 12:25:43 +00:00
} else {
2022-11-16 18:04:50 +00:00
user , err = dataprovider . GetUserWithGroupSettings ( username , "" )
2021-11-13 12:25:43 +00:00
email = user . Email
2023-02-27 18:02:43 +00:00
subject = fmt . Sprintf ( "Email Verification Code for user %q" , username )
2021-11-13 12:25:43 +00:00
if err == nil {
if ! isUserAllowedToResetPassword ( r , & user ) {
2023-12-10 15:40:13 +00:00
return util . NewI18nError (
util . NewValidationError ( "you are not allowed to reset your password" ) ,
util . I18nErrorPwdResetForbidded ,
)
2021-11-13 12:25:43 +00:00
}
}
}
if err != nil {
2023-01-25 17:49:03 +00:00
if errors . Is ( err , util . ErrNotFound ) {
2023-03-04 12:55:48 +00:00
handleDefenderEventLoginFailed ( util . GetIPFromRemoteAddress ( r . RemoteAddr ) , err ) //nolint:errcheck
2023-02-27 18:02:43 +00:00
logger . Debug ( logSender , middleware . GetReqID ( r . Context ( ) ) , "username %q does not exists, reset password request silently ignored, is admin? %v" ,
2021-11-13 12:25:43 +00:00
username , isAdmin )
return nil
}
2023-12-10 15:40:13 +00:00
return util . NewI18nError ( util . NewGenericError ( "Error retrieving your account, please try again later" ) , util . I18nErrorGetUser )
2021-11-13 12:25:43 +00:00
}
if email == "" {
2023-12-10 15:40:13 +00:00
return util . NewI18nError (
util . NewValidationError ( "Your account does not have an email address, it is not possible to reset your password by sending an email verification code" ) ,
util . I18nErrorPwdResetNoEmail ,
)
2021-11-13 12:25:43 +00:00
}
c := newResetCode ( username , isAdmin )
body := new ( bytes . Buffer )
data := make ( map [ string ] string )
data [ "Code" ] = c . Code
if err := smtp . RenderPasswordResetTemplate ( body , data ) ; err != nil {
logger . Warn ( logSender , middleware . GetReqID ( r . Context ( ) ) , "unable to render password reset template: %v" , err )
return util . NewGenericError ( "Unable to render password reset template" )
}
startTime := time . Now ( )
2023-05-25 17:55:27 +00:00
if err := smtp . SendEmail ( [ ] string { email } , nil , subject , body . String ( ) , smtp . EmailContentTypeTextHTML ) ; err != nil {
2021-11-13 12:25:43 +00:00
logger . Warn ( logSender , middleware . GetReqID ( r . Context ( ) ) , "unable to send password reset code via email: %v, elapsed: %v" ,
err , time . Since ( startTime ) )
2023-12-10 15:40:13 +00:00
return util . NewI18nError (
util . NewGenericError ( fmt . Sprintf ( "Error sending confirmation code via email: %v" , err ) ) ,
util . I18nErrorPwdResetSendEmail ,
)
2021-11-13 12:25:43 +00:00
}
2023-02-27 18:02:43 +00:00
logger . Debug ( logSender , middleware . GetReqID ( r . Context ( ) ) , "reset code sent via email to %q, email: %q, is admin? %v, elapsed: %v" ,
2021-11-13 12:25:43 +00:00
username , email , isAdmin , time . Since ( startTime ) )
2022-05-19 17:49:51 +00:00
return resetCodesMgr . Add ( c )
2021-11-13 12:25:43 +00:00
}
2023-12-30 18:12:22 +00:00
func handleResetPassword ( r * http . Request , code , newPassword , confirmPassword string , isAdmin bool ) (
2021-11-13 12:25:43 +00:00
* dataprovider . Admin , * dataprovider . User , error ,
) {
var admin dataprovider . Admin
var user dataprovider . User
var err error
if newPassword == "" {
2022-04-25 13:49:11 +00:00
return & admin , & user , util . NewValidationError ( "please set a password" )
2021-11-13 12:25:43 +00:00
}
if code == "" {
2022-04-25 13:49:11 +00:00
return & admin , & user , util . NewValidationError ( "please set a confirmation code" )
2021-11-13 12:25:43 +00:00
}
2023-12-30 18:12:22 +00:00
if newPassword != confirmPassword {
return & admin , & user , util . NewI18nError ( errors . New ( "the two password fields do not match" ) , util . I18nErrorChangePwdNoMatch )
}
2023-03-04 12:55:48 +00:00
ipAddr := util . GetIPFromRemoteAddress ( r . RemoteAddr )
2022-05-19 17:49:51 +00:00
resetCode , err := resetCodesMgr . Get ( code )
if err != nil {
2023-03-04 12:55:48 +00:00
handleDefenderEventLoginFailed ( ipAddr , dataprovider . ErrInvalidCredentials ) //nolint:errcheck
2022-04-25 13:49:11 +00:00
return & admin , & user , util . NewValidationError ( "confirmation code not found" )
2021-11-13 12:25:43 +00:00
}
if resetCode . IsAdmin != isAdmin {
2022-04-25 13:49:11 +00:00
return & admin , & user , util . NewValidationError ( "invalid confirmation code" )
2021-11-13 12:25:43 +00:00
}
if isAdmin {
admin , err = dataprovider . AdminExists ( resetCode . Username )
if err != nil {
2022-04-25 13:49:11 +00:00
return & admin , & user , util . NewValidationError ( "unable to associate the confirmation code with an existing admin" )
2021-11-13 12:25:43 +00:00
}
admin . Password = newPassword
2023-03-04 12:55:48 +00:00
err = dataprovider . UpdateAdmin ( & admin , dataprovider . ActionExecutorSelf , ipAddr , admin . Role )
2021-11-13 12:25:43 +00:00
if err != nil {
2022-04-25 13:49:11 +00:00
return & admin , & user , util . NewGenericError ( fmt . Sprintf ( "unable to set the new password: %v" , err ) )
2021-11-13 12:25:43 +00:00
}
2022-05-19 17:49:51 +00:00
err = resetCodesMgr . Delete ( code )
return & admin , & user , err
2022-04-25 13:49:11 +00:00
}
2022-11-16 18:04:50 +00:00
user , err = dataprovider . GetUserWithGroupSettings ( resetCode . Username , "" )
2022-04-25 13:49:11 +00:00
if err != nil {
return & admin , & user , util . NewValidationError ( "Unable to associate the confirmation code with an existing user" )
}
if err == nil {
if ! isUserAllowedToResetPassword ( r , & user ) {
2023-12-10 15:40:13 +00:00
return & admin , & user , util . NewI18nError (
util . NewValidationError ( "you are not allowed to reset your password" ) ,
util . I18nErrorPwdResetForbidded ,
)
2021-11-13 12:25:43 +00:00
}
}
2022-04-25 13:49:11 +00:00
err = dataprovider . UpdateUserPassword ( user . Username , newPassword , dataprovider . ActionExecutorSelf ,
2022-12-03 10:45:27 +00:00
util . GetIPFromRemoteAddress ( r . RemoteAddr ) , user . Role )
2022-04-25 13:49:11 +00:00
if err == nil {
2022-05-19 17:49:51 +00:00
err = resetCodesMgr . Delete ( code )
2022-04-25 13:49:11 +00:00
}
2023-05-16 17:15:45 +00:00
user . LastPasswordChange = util . GetTimeAsMsSinceEpoch ( time . Now ( ) )
user . Filters . RequirePasswordChange = false
2022-04-25 13:49:11 +00:00
return & admin , & user , err
2021-11-13 12:25:43 +00:00
}
func isUserAllowedToResetPassword ( r * http . Request , user * dataprovider . User ) bool {
if ! user . CanResetPassword ( ) {
return false
}
2022-05-19 17:49:51 +00:00
if util . Contains ( user . Filters . DeniedProtocols , common . ProtocolHTTP ) {
2021-11-13 12:25:43 +00:00
return false
}
2023-08-04 18:56:23 +00:00
if ! user . IsLoginMethodAllowed ( dataprovider . LoginMethodPassword , common . ProtocolHTTP ) {
2021-11-13 12:25:43 +00:00
return false
}
if ! user . IsLoginFromAddrAllowed ( r . RemoteAddr ) {
return false
}
return true
}
2022-02-19 09:53:35 +00:00
func getProtocolFromRequest ( r * http . Request ) string {
if isLoggedInWithOIDC ( r ) {
return common . ProtocolOIDC
}
return common . ProtocolHTTP
}
2023-04-18 16:11:23 +00:00
func hideConfidentialData ( claims * jwtTokenClaims , r * http . Request ) bool {
if ! claims . hasPerm ( dataprovider . PermAdminManageSystem ) {
return true
}
return r . URL . Query ( ) . Get ( "confidential_data" ) != "1"
}