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"
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"
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"
2020-05-06 17:36:34 +00:00
2021-06-26 05:31:41 +00:00
"github.com/drakkan/sftpgo/v2/common"
"github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/logger"
2021-07-11 13:26:51 +00:00
"github.com/drakkan/sftpgo/v2/metric"
2021-10-23 13:47:21 +00:00
"github.com/drakkan/sftpgo/v2/sdk/plugin"
2021-11-13 12:25:43 +00:00
"github.com/drakkan/sftpgo/v2/smtp"
2021-07-11 13:26:51 +00:00
"github.com/drakkan/sftpgo/v2/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
2021-08-17 16:08:32 +00:00
if _ , ok := err . ( * util . RecordNotFoundError ) ; ok {
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 {
2021-07-11 13:26:51 +00:00
if _ , ok := err . ( * util . ValidationError ) ; ok {
2019-10-07 16:19:01 +00:00
return http . StatusBadRequest
}
2021-07-11 13:26:51 +00:00
if _ , ok := err . ( * util . MethodDisabledError ) ; ok {
2019-10-07 16:19:01 +00:00
return http . StatusForbidden
}
2021-07-11 13:26:51 +00:00
if _ , ok := err . ( * util . RecordNotFoundError ) ; ok {
2020-06-20 10:38:04 +00:00
return http . StatusNotFound
}
2019-12-27 22:12:44 +00:00
if os . IsNotExist ( err ) {
return http . StatusBadRequest
}
2021-10-09 12:17:28 +00:00
if os . IsPermission ( err ) {
return http . StatusForbidden
}
2021-10-23 13:47:21 +00:00
if errors . Is ( err , plugin . ErrNoSearcher ) {
return http . StatusNotImplemented
}
2019-10-07 16:19:01 +00:00
return http . StatusInternalServerError
}
2021-06-05 14:07:09 +00:00
func getMappedStatusCode ( err error ) int {
var statusCode int
switch err {
case os . ErrPermission :
statusCode = http . StatusForbidden
case os . ErrNotExist :
statusCode = http . StatusNotFound
2021-11-22 11:25:36 +00:00
case common . ErrQuotaExceeded :
statusCode = http . StatusRequestEntityTooLarge
2021-06-05 14:07:09 +00:00
default :
statusCode = http . StatusInternalServerError
}
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 )
}
}
return util . RemoveDuplicates ( result )
}
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 )
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
}
2021-01-17 21:29:08 +00:00
if common . Connections . Close ( connectionID ) {
sendAPIResponse ( w , r , nil , "Connection closed" , http . StatusOK )
2020-06-07 21:30:18 +00:00
} else {
2021-01-17 21:29:08 +00:00
sendAPIResponse ( w , r , nil , "Not Found" , http . StatusNotFound )
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
2021-11-06 13:13:20 +00:00
func renderCompressedFiles ( w http . ResponseWriter , conn * Connection , baseDir string , files [ ] string ,
share * dataprovider . Share ,
) {
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 {
fullPath := path . Join ( baseDir , file )
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 {
conn . Log ( logger . LevelWarn , "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 {
conn . Log ( logger . LevelDebug , "unable to add zip entry %#v, stat error: %v" , entryPath , err )
return err
}
if info . IsDir ( ) {
_ , err := wr . Create ( getZipEntryName ( entryPath , baseDir ) + "/" )
if err != nil {
conn . Log ( logger . LevelDebug , "unable to create zip entry %#v: %v" , entryPath , err )
return err
}
contents , err := conn . ReadDir ( entryPath )
if err != nil {
conn . Log ( logger . LevelDebug , "unable to add zip entry %#v, read dir error: %v" , entryPath , err )
return err
}
for _ , info := range contents {
fullPath := path . Join ( entryPath , info . Name ( ) )
if err := addZipEntry ( wr , conn , fullPath , baseDir ) ; err != nil {
return err
}
}
return nil
}
if ! info . Mode ( ) . IsRegular ( ) {
// we only allow regular files
conn . Log ( logger . LevelDebug , "skipping zip entry for non regular file %#v" , entryPath )
return nil
}
reader , err := conn . getFileReader ( entryPath , 0 , http . MethodGet )
if err != nil {
conn . Log ( logger . LevelDebug , "unable to add zip entry %#v, cannot open file: %v" , entryPath , err )
return err
}
defer reader . Close ( )
f , err := wr . Create ( getZipEntryName ( entryPath , baseDir ) )
if err != nil {
conn . Log ( logger . LevelDebug , "unable to create zip entry %#v: %v" , entryPath , err )
return err
}
_ , err = io . Copy ( f , reader )
return err
}
func getZipEntryName ( entryPath , baseDir string ) string {
entryPath = strings . TrimPrefix ( entryPath , baseDir )
return strings . TrimPrefix ( entryPath , "/" )
}
2021-06-05 14:07:09 +00:00
2021-11-25 18:24:32 +00:00
func downloadFile ( w http . ResponseWriter , r * http . Request , connection * Connection , name string ,
info os . FileInfo , inline bool ,
) ( int , error ) {
2021-06-05 14:07:09 +00:00
var err error
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 , "," ) {
return http . StatusRequestedRangeNotSatisfiable , fmt . Errorf ( "unsupported range %#v" , rangeHeader )
}
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 {
return getMappedStatusCode ( err ) , fmt . Errorf ( "unable to read file %#v: %v" , name , err )
}
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 {
w . Header ( ) . Set ( "Content-Disposition" , fmt . Sprintf ( "attachment; filename=%#v" , path . Base ( name ) ) )
}
2021-06-05 14:07:09 +00:00
w . Header ( ) . Set ( "Accept-Ranges" , "bytes" )
w . WriteHeader ( responseStatus )
if r . Method != http . MethodHead {
io . CopyN ( w , reader , size ) //nolint:errcheck
}
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
}
if modtime . Add ( 60 * time . Second ) . Before ( t ) {
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 {
return 0 , 0 , fmt . Errorf ( "unsupported range %#v" , bytesRange )
}
if end > 0 {
if start == - 1 {
// we have something like -500
start = size - end
size = end
// start cannit be < 0 here, we did end = size -1 above
} else {
// we have something like 500-600
size = end - start + 1
if size < 0 {
return 0 , 0 , fmt . Errorf ( "unacceptable range %#v" , bytesRange )
}
}
return start , size , nil
}
// we have something like 500-
size -= start
if size < 0 {
return 0 , 0 , fmt . Errorf ( "unacceptable range %#v" , bytesRange )
}
return start , size , err
}
func updateLoginMetrics ( user * dataprovider . User , ip string , err error ) {
2021-07-11 13:26:51 +00:00
metric . AddLoginAttempt ( dataprovider . LoginMethodPassword )
2021-09-11 16:23:11 +00:00
if err != nil && err != common . ErrInternalFailure && err != common . ErrNoCredentials {
2021-06-05 14:07:09 +00:00
logger . ConnectionFailedLog ( user . Username , ip , dataprovider . LoginMethodPassword , common . ProtocolHTTP , err . Error ( ) )
event := common . HostEventLoginFailed
2021-07-11 13:26:51 +00:00
if _ , ok := err . ( * util . RecordNotFoundError ) ; ok {
2021-06-05 14:07:09 +00:00
event = common . HostEventUserNotFound
}
common . AddDefenderEvent ( ip , event )
}
2021-07-11 13:26:51 +00:00
metric . AddLoginResult ( dataprovider . LoginMethodPassword , err )
2021-06-05 14:07:09 +00:00
dataprovider . ExecutePostLoginHook ( user , dataprovider . LoginMethodPassword , ip , common . ProtocolHTTP , err )
}
func checkHTTPClientUser ( user * dataprovider . User , r * http . Request , connectionID string ) error {
2021-07-11 13:26:51 +00:00
if util . IsStringInSlice ( common . ProtocolHTTP , user . Filters . DeniedProtocols ) {
2021-12-02 18:36:42 +00:00
logger . Info ( logSender , connectionID , "cannot login user %#v, protocol HTTP is not allowed" , user . Username )
2021-06-05 14:07:09 +00:00
return fmt . Errorf ( "protocol HTTP is not allowed for user %#v" , user . Username )
}
if ! user . IsLoginMethodAllowed ( dataprovider . LoginMethodPassword , nil ) {
2021-12-02 18:36:42 +00:00
logger . Info ( logSender , connectionID , "cannot login user %#v, password login method is not allowed" , user . Username )
2021-06-05 14:07:09 +00:00
return fmt . Errorf ( "login method password is not allowed for user %#v" , user . Username )
}
if user . MaxSessions > 0 {
activeSessions := common . Connections . GetActiveSessions ( user . Username )
if activeSessions >= user . MaxSessions {
2021-12-02 18:36:42 +00:00
logger . Info ( logSender , connectionID , "authentication refused for user: %#v, too many open sessions: %v/%v" , user . Username ,
2021-06-05 14:07:09 +00:00
activeSessions , user . MaxSessions )
return fmt . Errorf ( "too many open sessions: %v" , activeSessions )
}
}
if ! user . IsLoginFromAddrAllowed ( r . RemoteAddr ) {
2021-12-02 18:36:42 +00:00
logger . Info ( logSender , connectionID , "cannot login user %#v, remote address is not allowed: %v" , user . Username , r . RemoteAddr )
2021-06-05 14:07:09 +00:00
return fmt . Errorf ( "login for user %#v is not allowed from this address: %v" , user . Username , r . RemoteAddr )
}
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 == "" {
return util . NewValidationError ( "Username is mandatory" )
}
if isAdmin {
admin , err = dataprovider . AdminExists ( username )
email = admin . Email
subject = fmt . Sprintf ( "Email Verification Code for admin %#v" , username )
} else {
user , err = dataprovider . UserExists ( username )
email = user . Email
subject = fmt . Sprintf ( "Email Verification Code for user %#v" , username )
if err == nil {
if ! isUserAllowedToResetPassword ( r , & user ) {
return util . NewValidationError ( "You are not allowed to reset your password" )
}
}
}
if err != nil {
if _ , ok := err . ( * util . RecordNotFoundError ) ; ok {
logger . Debug ( logSender , middleware . GetReqID ( r . Context ( ) ) , "username %#v does not exists, reset password request silently ignored, is admin? %v" ,
username , isAdmin )
return nil
}
return util . NewGenericError ( "Error retrieving your account, please try again later" )
}
if email == "" {
return util . NewValidationError ( "Your account does not have an email address, it is not possible to reset your password by sending an email verification code" )
}
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 ( )
if err := smtp . SendEmail ( email , subject , body . String ( ) , smtp . EmailContentTypeTextHTML ) ; err != nil {
logger . Warn ( logSender , middleware . GetReqID ( r . Context ( ) ) , "unable to send password reset code via email: %v, elapsed: %v" ,
err , time . Since ( startTime ) )
return util . NewGenericError ( fmt . Sprintf ( "Unable to send confirmation code via email: %v" , err ) )
}
logger . Debug ( logSender , middleware . GetReqID ( r . Context ( ) ) , "reset code sent via email to %#v, email: %#v, is admin? %v, elapsed: %v" ,
username , email , isAdmin , time . Since ( startTime ) )
resetCodes . Store ( c . Code , c )
return nil
}
func handleResetPassword ( r * http . Request , code , newPassword string , isAdmin bool ) (
* dataprovider . Admin , * dataprovider . User , error ,
) {
var admin dataprovider . Admin
var user dataprovider . User
var err error
if newPassword == "" {
return & admin , & user , util . NewValidationError ( "Please set a password" )
}
if code == "" {
return & admin , & user , util . NewValidationError ( "Please set a confirmation code" )
}
c , ok := resetCodes . Load ( code )
if ! ok {
return & admin , & user , util . NewValidationError ( "Confirmation code not found" )
}
resetCode := c . ( * resetCode )
if resetCode . IsAdmin != isAdmin {
return & admin , & user , util . NewValidationError ( "Invalid confirmation code" )
}
if isAdmin {
admin , err = dataprovider . AdminExists ( resetCode . Username )
if err != nil {
return & admin , & user , util . NewValidationError ( "Unable to associate the confirmation code with an existing admin" )
}
admin . Password = newPassword
err = dataprovider . UpdateAdmin ( & admin , admin . Username , util . GetIPFromRemoteAddress ( r . RemoteAddr ) )
if err != nil {
return & admin , & user , util . NewGenericError ( fmt . Sprintf ( "Unable to set the new password: %v" , err ) )
}
} else {
user , err = dataprovider . UserExists ( resetCode . Username )
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 ) {
return & admin , & user , util . NewValidationError ( "You are not allowed to reset your password" )
}
}
user . Password = newPassword
err = dataprovider . UpdateUser ( & user , user . Username , util . GetIPFromRemoteAddress ( r . RemoteAddr ) )
if err != nil {
return & admin , & user , util . NewGenericError ( fmt . Sprintf ( "Unable to set the new password: %v" , err ) )
}
}
resetCodes . Delete ( code )
return & admin , & user , nil
}
func isUserAllowedToResetPassword ( r * http . Request , user * dataprovider . User ) bool {
if ! user . CanResetPassword ( ) {
return false
}
if util . IsStringInSlice ( common . ProtocolHTTP , user . Filters . DeniedProtocols ) {
return false
}
if ! user . IsLoginMethodAllowed ( dataprovider . LoginMethodPassword , nil ) {
return false
}
if ! user . IsLoginFromAddrAllowed ( r . RemoteAddr ) {
return false
}
return true
}