github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/ftp-server-driver.go (about) 1 // Copyright (c) 2015-2023 MinIO, Inc. 2 // 3 // This file is part of MinIO Object Storage stack 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package cmd 19 20 import ( 21 "bytes" 22 "context" 23 "crypto/subtle" 24 "errors" 25 "fmt" 26 "io" 27 "os" 28 "strings" 29 "time" 30 31 "github.com/minio/madmin-go/v3" 32 "github.com/minio/minio-go/v7" 33 "github.com/minio/minio-go/v7/pkg/credentials" 34 "github.com/minio/minio/internal/auth" 35 xioutil "github.com/minio/minio/internal/ioutil" 36 "github.com/minio/minio/internal/logger" 37 ftp "goftp.io/server/v2" 38 ) 39 40 var _ ftp.Driver = &ftpDriver{} 41 42 // ftpDriver implements ftpDriver to store files in minio 43 type ftpDriver struct { 44 endpoint string 45 } 46 47 // NewFTPDriver implements ftp.Driver interface 48 func NewFTPDriver() ftp.Driver { 49 return &ftpDriver{endpoint: fmt.Sprintf("127.0.0.1:%s", globalMinioPort)} 50 } 51 52 func buildMinioPath(p string) string { 53 return strings.TrimPrefix(p, SlashSeparator) 54 } 55 56 func buildMinioDir(p string) string { 57 v := buildMinioPath(p) 58 if !strings.HasSuffix(v, SlashSeparator) { 59 return v + SlashSeparator 60 } 61 return v 62 } 63 64 type minioFileInfo struct { 65 p string 66 info minio.ObjectInfo 67 isDir bool 68 } 69 70 func (m *minioFileInfo) Name() string { 71 return m.p 72 } 73 74 func (m *minioFileInfo) Size() int64 { 75 return m.info.Size 76 } 77 78 func (m *minioFileInfo) Mode() os.FileMode { 79 if m.isDir { 80 return os.ModeDir 81 } 82 return os.ModePerm 83 } 84 85 func (m *minioFileInfo) ModTime() time.Time { 86 return m.info.LastModified 87 } 88 89 func (m *minioFileInfo) IsDir() bool { 90 return m.isDir 91 } 92 93 func (m *minioFileInfo) Sys() interface{} { 94 return nil 95 } 96 97 //msgp:ignore ftpMetrics 98 type ftpMetrics struct{} 99 100 var globalFtpMetrics ftpMetrics 101 102 func ftpTrace(s *ftp.Context, startTime time.Time, source, path string, err error) madmin.TraceInfo { 103 var errStr string 104 if err != nil { 105 errStr = err.Error() 106 } 107 return madmin.TraceInfo{ 108 TraceType: madmin.TraceFTP, 109 Time: startTime, 110 NodeName: globalLocalNodeName, 111 FuncName: fmt.Sprintf("ftp USER=%s COMMAND=%s PARAM=%s ISLOGIN=%t, Source=%s", s.Sess.LoginUser(), s.Cmd, s.Param, s.Sess.IsLogin(), source), 112 Duration: time.Since(startTime), 113 Path: path, 114 Error: errStr, 115 } 116 } 117 118 func (m *ftpMetrics) log(s *ftp.Context, paths ...string) func(err error) { 119 startTime := time.Now() 120 source := getSource(2) 121 return func(err error) { 122 globalTrace.Publish(ftpTrace(s, startTime, source, strings.Join(paths, " "), err)) 123 } 124 } 125 126 // Stat implements ftpDriver 127 func (driver *ftpDriver) Stat(ctx *ftp.Context, path string) (fi os.FileInfo, err error) { 128 stopFn := globalFtpMetrics.log(ctx, path) 129 defer stopFn(err) 130 131 if path == SlashSeparator { 132 return &minioFileInfo{ 133 p: SlashSeparator, 134 isDir: true, 135 }, nil 136 } 137 138 bucket, object := path2BucketObject(path) 139 if bucket == "" { 140 return nil, errors.New("bucket name cannot be empty") 141 } 142 143 clnt, err := driver.getMinIOClient(ctx) 144 if err != nil { 145 return nil, err 146 } 147 148 if object == "" { 149 ok, err := clnt.BucketExists(context.Background(), bucket) 150 if err != nil { 151 return nil, err 152 } 153 if !ok { 154 return nil, os.ErrNotExist 155 } 156 return &minioFileInfo{ 157 p: pathClean(bucket), 158 info: minio.ObjectInfo{Key: bucket}, 159 isDir: true, 160 }, nil 161 } 162 163 objInfo, err := clnt.StatObject(context.Background(), bucket, object, minio.StatObjectOptions{}) 164 if err != nil { 165 if minio.ToErrorResponse(err).Code == "NoSuchKey" { 166 // dummy return to satisfy LIST (stat -> list) behavior. 167 return &minioFileInfo{ 168 p: pathClean(object), 169 info: minio.ObjectInfo{Key: object}, 170 isDir: true, 171 }, nil 172 } 173 return nil, err 174 } 175 176 isDir := strings.HasSuffix(objInfo.Key, SlashSeparator) 177 return &minioFileInfo{ 178 p: pathClean(object), 179 info: objInfo, 180 isDir: isDir, 181 }, nil 182 } 183 184 // ListDir implements ftpDriver 185 func (driver *ftpDriver) ListDir(ctx *ftp.Context, path string, callback func(os.FileInfo) error) (err error) { 186 stopFn := globalFtpMetrics.log(ctx, path) 187 defer stopFn(err) 188 189 clnt, err := driver.getMinIOClient(ctx) 190 if err != nil { 191 return err 192 } 193 194 cctx, cancel := context.WithCancel(context.Background()) 195 defer cancel() 196 197 bucket, prefix := path2BucketObject(path) 198 if bucket == "" { 199 buckets, err := clnt.ListBuckets(cctx) 200 if err != nil { 201 return err 202 } 203 204 for _, bucket := range buckets { 205 info := minioFileInfo{ 206 p: pathClean(bucket.Name), 207 info: minio.ObjectInfo{Key: retainSlash(bucket.Name), LastModified: bucket.CreationDate}, 208 isDir: true, 209 } 210 if err := callback(&info); err != nil { 211 return err 212 } 213 } 214 215 return nil 216 } 217 218 prefix = retainSlash(prefix) 219 220 for object := range clnt.ListObjects(cctx, bucket, minio.ListObjectsOptions{ 221 Prefix: prefix, 222 Recursive: false, 223 }) { 224 if object.Err != nil { 225 return object.Err 226 } 227 228 if object.Key == prefix { 229 continue 230 } 231 232 isDir := strings.HasSuffix(object.Key, SlashSeparator) 233 info := minioFileInfo{ 234 p: pathClean(strings.TrimPrefix(object.Key, prefix)), 235 info: object, 236 isDir: isDir, 237 } 238 239 if err := callback(&info); err != nil { 240 return err 241 } 242 } 243 244 return nil 245 } 246 247 func (driver *ftpDriver) CheckPasswd(c *ftp.Context, username, password string) (ok bool, err error) { 248 stopFn := globalFtpMetrics.log(c, username) 249 defer stopFn(err) 250 251 if globalIAMSys.LDAPConfig.Enabled() { 252 sa, _, err := globalIAMSys.getServiceAccount(context.Background(), username) 253 if err != nil && !errors.Is(err, errNoSuchServiceAccount) { 254 return false, err 255 } 256 if errors.Is(err, errNoSuchServiceAccount) { 257 ldapUserDN, groupDistNames, err := globalIAMSys.LDAPConfig.Bind(username, password) 258 if err != nil { 259 return false, err 260 } 261 ldapPolicies, _ := globalIAMSys.PolicyDBGet(ldapUserDN, groupDistNames...) 262 return len(ldapPolicies) > 0, nil 263 } 264 return subtle.ConstantTimeCompare([]byte(sa.Credentials.SecretKey), []byte(password)) == 1, nil 265 } 266 267 ui, ok := globalIAMSys.GetUser(context.Background(), username) 268 if !ok { 269 return false, nil 270 } 271 return subtle.ConstantTimeCompare([]byte(ui.Credentials.SecretKey), []byte(password)) == 1, nil 272 } 273 274 func (driver *ftpDriver) getMinIOClient(ctx *ftp.Context) (*minio.Client, error) { 275 ui, ok := globalIAMSys.GetUser(context.Background(), ctx.Sess.LoginUser()) 276 if !ok && !globalIAMSys.LDAPConfig.Enabled() { 277 return nil, errNoSuchUser 278 } 279 if !ok && globalIAMSys.LDAPConfig.Enabled() { 280 sa, _, err := globalIAMSys.getServiceAccount(context.Background(), ctx.Sess.LoginUser()) 281 if err != nil && !errors.Is(err, errNoSuchServiceAccount) { 282 return nil, err 283 } 284 285 var mcreds *credentials.Credentials 286 if errors.Is(err, errNoSuchServiceAccount) { 287 targetUser, targetGroups, err := globalIAMSys.LDAPConfig.LookupUserDN(ctx.Sess.LoginUser()) 288 if err != nil { 289 return nil, err 290 } 291 ldapPolicies, _ := globalIAMSys.PolicyDBGet(targetUser, targetGroups...) 292 if len(ldapPolicies) == 0 { 293 return nil, errAuthentication 294 } 295 expiryDur, err := globalIAMSys.LDAPConfig.GetExpiryDuration("") 296 if err != nil { 297 return nil, err 298 } 299 claims := make(map[string]interface{}) 300 claims[expClaim] = UTCNow().Add(expiryDur).Unix() 301 claims[ldapUser] = targetUser 302 claims[ldapUserN] = ctx.Sess.LoginUser() 303 304 cred, err := auth.GetNewCredentialsWithMetadata(claims, globalActiveCred.SecretKey) 305 if err != nil { 306 return nil, err 307 } 308 309 // Set the parent of the temporary access key, this is useful 310 // in obtaining service accounts by this cred. 311 cred.ParentUser = targetUser 312 313 // Set this value to LDAP groups, LDAP user can be part 314 // of large number of groups 315 cred.Groups = targetGroups 316 317 // Set the newly generated credentials, policyName is empty on purpose 318 // LDAP policies are applied automatically using their ldapUser, ldapGroups 319 // mapping. 320 updatedAt, err := globalIAMSys.SetTempUser(context.Background(), cred.AccessKey, cred, "") 321 if err != nil { 322 return nil, err 323 } 324 325 // Call hook for site replication. 326 logger.LogIf(context.Background(), globalSiteReplicationSys.IAMChangeHook(context.Background(), madmin.SRIAMItem{ 327 Type: madmin.SRIAMItemSTSAcc, 328 STSCredential: &madmin.SRSTSCredential{ 329 AccessKey: cred.AccessKey, 330 SecretKey: cred.SecretKey, 331 SessionToken: cred.SessionToken, 332 ParentUser: cred.ParentUser, 333 }, 334 UpdatedAt: updatedAt, 335 })) 336 337 mcreds = credentials.NewStaticV4(cred.AccessKey, cred.SecretKey, cred.SessionToken) 338 } else { 339 mcreds = credentials.NewStaticV4(sa.Credentials.AccessKey, sa.Credentials.SecretKey, "") 340 } 341 342 return minio.New(driver.endpoint, &minio.Options{ 343 Creds: mcreds, 344 Secure: globalIsTLS, 345 Transport: globalRemoteFTPClientTransport, 346 }) 347 } 348 349 // ok == true - at this point 350 351 if ui.Credentials.IsTemp() { 352 // Temporary credentials are not allowed. 353 return nil, errAuthentication 354 } 355 356 return minio.New(driver.endpoint, &minio.Options{ 357 Creds: credentials.NewStaticV4(ui.Credentials.AccessKey, ui.Credentials.SecretKey, ""), 358 Secure: globalIsTLS, 359 Transport: globalRemoteFTPClientTransport, 360 }) 361 } 362 363 // DeleteDir implements ftpDriver 364 func (driver *ftpDriver) DeleteDir(ctx *ftp.Context, path string) (err error) { 365 stopFn := globalFtpMetrics.log(ctx, path) 366 defer stopFn(err) 367 368 bucket, prefix := path2BucketObject(path) 369 if bucket == "" { 370 return errors.New("deleting all buckets not allowed") 371 } 372 373 clnt, err := driver.getMinIOClient(ctx) 374 if err != nil { 375 return err 376 } 377 378 cctx, cancel := context.WithCancel(context.Background()) 379 defer cancel() 380 381 if prefix == "" { 382 // if all objects are not deleted yet this call may fail. 383 return clnt.RemoveBucket(cctx, bucket) 384 } 385 386 objectsCh := make(chan minio.ObjectInfo) 387 388 // Send object names that are needed to be removed to objectsCh 389 go func() { 390 defer xioutil.SafeClose(objectsCh) 391 opts := minio.ListObjectsOptions{ 392 Prefix: prefix, 393 Recursive: true, 394 } 395 for object := range clnt.ListObjects(cctx, bucket, opts) { 396 if object.Err != nil { 397 return 398 } 399 objectsCh <- object 400 } 401 }() 402 403 // Call RemoveObjects API 404 for err := range clnt.RemoveObjects(context.Background(), bucket, objectsCh, minio.RemoveObjectsOptions{}) { 405 if err.Err != nil { 406 return err.Err 407 } 408 } 409 410 return nil 411 } 412 413 // DeleteFile implements ftpDriver 414 func (driver *ftpDriver) DeleteFile(ctx *ftp.Context, path string) (err error) { 415 stopFn := globalFtpMetrics.log(ctx, path) 416 defer stopFn(err) 417 418 bucket, object := path2BucketObject(path) 419 if bucket == "" { 420 return errors.New("bucket name cannot be empty") 421 } 422 423 clnt, err := driver.getMinIOClient(ctx) 424 if err != nil { 425 return err 426 } 427 428 return clnt.RemoveObject(context.Background(), bucket, object, minio.RemoveObjectOptions{}) 429 } 430 431 // Rename implements ftpDriver 432 func (driver *ftpDriver) Rename(ctx *ftp.Context, fromPath string, toPath string) (err error) { 433 stopFn := globalFtpMetrics.log(ctx, fromPath, toPath) 434 defer stopFn(err) 435 436 return NotImplemented{} 437 } 438 439 // MakeDir implements ftpDriver 440 func (driver *ftpDriver) MakeDir(ctx *ftp.Context, path string) (err error) { 441 stopFn := globalFtpMetrics.log(ctx, path) 442 defer stopFn(err) 443 444 bucket, prefix := path2BucketObject(path) 445 if bucket == "" { 446 return errors.New("bucket name cannot be empty") 447 } 448 449 clnt, err := driver.getMinIOClient(ctx) 450 if err != nil { 451 return err 452 } 453 454 if prefix == "" { 455 return clnt.MakeBucket(context.Background(), bucket, minio.MakeBucketOptions{Region: globalSite.Region}) 456 } 457 458 dirPath := buildMinioDir(prefix) 459 460 _, err = clnt.PutObject(context.Background(), bucket, dirPath, bytes.NewReader([]byte("")), 0, 461 // Always send Content-MD5 to succeed with bucket with 462 // locking enabled. There is no performance hit since 463 // this is always an empty object 464 minio.PutObjectOptions{SendContentMd5: true}, 465 ) 466 return err 467 } 468 469 // GetFile implements ftpDriver 470 func (driver *ftpDriver) GetFile(ctx *ftp.Context, path string, offset int64) (n int64, rc io.ReadCloser, err error) { 471 stopFn := globalFtpMetrics.log(ctx, path) 472 defer stopFn(err) 473 474 bucket, object := path2BucketObject(path) 475 if bucket == "" { 476 return 0, nil, errors.New("bucket name cannot be empty") 477 } 478 479 clnt, err := driver.getMinIOClient(ctx) 480 if err != nil { 481 return 0, nil, err 482 } 483 484 opts := minio.GetObjectOptions{} 485 obj, err := clnt.GetObject(context.Background(), bucket, object, opts) 486 if err != nil { 487 return 0, nil, err 488 } 489 defer func() { 490 if err != nil && obj != nil { 491 obj.Close() 492 } 493 }() 494 495 _, err = obj.Seek(offset, io.SeekStart) 496 if err != nil { 497 return 0, nil, err 498 } 499 500 info, err := obj.Stat() 501 if err != nil { 502 return 0, nil, err 503 } 504 505 return info.Size - offset, obj, nil 506 } 507 508 // PutFile implements ftpDriver 509 func (driver *ftpDriver) PutFile(ctx *ftp.Context, path string, data io.Reader, offset int64) (n int64, err error) { 510 stopFn := globalFtpMetrics.log(ctx, path) 511 defer stopFn(err) 512 513 bucket, object := path2BucketObject(path) 514 if bucket == "" { 515 return 0, errors.New("bucket name cannot be empty") 516 } 517 518 if offset != -1 { 519 // FTP - APPEND not implemented 520 return 0, NotImplemented{} 521 } 522 523 clnt, err := driver.getMinIOClient(ctx) 524 if err != nil { 525 return 0, err 526 } 527 528 info, err := clnt.PutObject(context.Background(), bucket, object, data, -1, minio.PutObjectOptions{ 529 ContentType: "application/octet-stream", 530 SendContentMd5: true, 531 }) 532 return info.Size, err 533 }