VirtualBox

source: vbox/trunk/src/VBox/Runtime/tools/RTFTPServer.cpp@ 82842

最後變更 在這個檔案從82842是 82842,由 vboxsync 提交於 5 年 前

IPRT/FTP: Retrieving files in sub directories also works now. bugref:9646

  • 屬性 svn:eol-style 設為 native
  • 屬性 svn:keywords 設為 Author Date Id Revision
檔案大小: 19.1 KB
 
1/* $Id: RTFTPServer.cpp 82842 2020-01-23 10:16:23Z vboxsync $ */
2/** @file
3 * IPRT - Utility for running a (simple) FTP server.
4 */
5
6/*
7 * Copyright (C) 2020 Oracle Corporation
8 *
9 * This file is part of VirtualBox Open Source Edition (OSE), as
10 * available from http://www.alldomusa.eu.org. This file is free software;
11 * you can redistribute it and/or modify it under the terms of the GNU
12 * General Public License (GPL) as published by the Free Software
13 * Foundation, in version 2 as it comes in the "COPYING" file of the
14 * VirtualBox OSE distribution. VirtualBox OSE is distributed in the
15 * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
16 *
17 * The contents of this file may alternatively be used under the terms
18 * of the Common Development and Distribution License Version 1.0
19 * (CDDL) only, as it comes in the "COPYING.CDDL" file of the
20 * VirtualBox OSE distribution, in which case the provisions of the
21 * CDDL are applicable instead of those of the GPL.
22 *
23 * You may elect to license modified versions of this file under the
24 * terms and conditions of either the GPL or the CDDL or both.
25 */
26
27/*
28 * Use this setup to best see what's going on:
29 *
30 * VBOX_LOG=rt_ftp=~0
31 * VBOX_LOG_DEST="nofile stderr"
32 * VBOX_LOG_FLAGS="unbuffered enabled thread msprog"
33 *
34 */
35
36
37/*********************************************************************************************************************************
38* Header Files *
39*********************************************************************************************************************************/
40#include <signal.h>
41
42#include <iprt/ftp.h>
43
44#include <iprt/net.h> /* To make use of IPv4Addr in RTGETOPTUNION. */
45
46#include <iprt/asm.h>
47#include <iprt/assert.h>
48#include <iprt/ctype.h>
49#include <iprt/err.h>
50#include <iprt/file.h>
51#include <iprt/getopt.h>
52#include <iprt/initterm.h>
53#include <iprt/mem.h>
54#include <iprt/message.h>
55#include <iprt/path.h>
56#include <iprt/stream.h>
57#include <iprt/string.h>
58#include <iprt/thread.h>
59#include <iprt/vfs.h>
60
61#ifdef RT_OS_WINDOWS
62# include <iprt/win/windows.h>
63#endif
64
65
66/*********************************************************************************************************************************
67* Definitations *
68*********************************************************************************************************************************/
69typedef struct FTPSERVERDATA
70{
71 /** The absolute path of the FTP server's root directory. */
72 char szPathRootAbs[RTPATH_MAX];
73 /** The relative current working directory (CWD) to szRootDir. */
74 char szCWD[RTPATH_MAX];
75 RTFILE hFile;
76} FTPSERVERDATA;
77typedef FTPSERVERDATA *PFTPSERVERDATA;
78
79typedef struct FTPDIRHANDLE
80{
81 /** The VFS (chain) handle to use for this directory. */
82 RTVFSDIR hVfsDir;
83} FTPDIRHANDLE;
84typedef FTPDIRHANDLE *PFTPDIRHANDLE;
85
86
87/*********************************************************************************************************************************
88* Global Variables *
89*********************************************************************************************************************************/
90/** Set by the signal handler when the FTP server shall be terminated. */
91static volatile bool g_fCanceled = false;
92static FTPSERVERDATA g_FTPServerData;
93
94
95#ifdef RT_OS_WINDOWS
96static BOOL WINAPI signalHandler(DWORD dwCtrlType)
97{
98 bool fEventHandled = FALSE;
99 switch (dwCtrlType)
100 {
101 /* User pressed CTRL+C or CTRL+BREAK or an external event was sent
102 * via GenerateConsoleCtrlEvent(). */
103 case CTRL_BREAK_EVENT:
104 case CTRL_CLOSE_EVENT:
105 case CTRL_C_EVENT:
106 ASMAtomicWriteBool(&g_fCanceled, true);
107 fEventHandled = TRUE;
108 break;
109 default:
110 break;
111 /** @todo Add other events here. */
112 }
113
114 return fEventHandled;
115}
116#else /* !RT_OS_WINDOWS */
117/**
118 * Signal handler that sets g_fCanceled.
119 *
120 * This can be executed on any thread in the process, on Windows it may even be
121 * a thread dedicated to delivering this signal. Don't do anything
122 * unnecessary here.
123 */
124static void signalHandler(int iSignal)
125{
126 NOREF(iSignal);
127 ASMAtomicWriteBool(&g_fCanceled, true);
128}
129#endif
130
131/**
132 * Installs a custom signal handler to get notified
133 * whenever the user wants to intercept the program.
134 *
135 * @todo Make this handler available for all VBoxManage modules?
136 */
137static int signalHandlerInstall(void)
138{
139 g_fCanceled = false;
140
141 int rc = VINF_SUCCESS;
142#ifdef RT_OS_WINDOWS
143 if (!SetConsoleCtrlHandler((PHANDLER_ROUTINE)signalHandler, TRUE /* Add handler */))
144 {
145 rc = RTErrConvertFromWin32(GetLastError());
146 RTMsgError("Unable to install console control handler, rc=%Rrc\n", rc);
147 }
148#else
149 signal(SIGINT, signalHandler);
150 signal(SIGTERM, signalHandler);
151# ifdef SIGBREAK
152 signal(SIGBREAK, signalHandler);
153# endif
154#endif
155 return rc;
156}
157
158/**
159 * Uninstalls a previously installed signal handler.
160 */
161static int signalHandlerUninstall(void)
162{
163 int rc = VINF_SUCCESS;
164#ifdef RT_OS_WINDOWS
165 if (!SetConsoleCtrlHandler((PHANDLER_ROUTINE)NULL, FALSE /* Remove handler */))
166 {
167 rc = RTErrConvertFromWin32(GetLastError());
168 RTMsgError("Unable to uninstall console control handler, rc=%Rrc\n", rc);
169 }
170#else
171 signal(SIGINT, SIG_DFL);
172 signal(SIGTERM, SIG_DFL);
173# ifdef SIGBREAK
174 signal(SIGBREAK, SIG_DFL);
175# endif
176#endif
177 return rc;
178}
179
180static DECLCALLBACK(int) onUserConnect(PRTFTPCALLBACKDATA pData, const char *pcszUser)
181{
182 RT_NOREF(pData, pcszUser);
183
184 RTPrintf("User '%s' connected\n", pcszUser);
185
186 return VINF_SUCCESS;
187}
188
189static DECLCALLBACK(int) onUserAuthenticate(PRTFTPCALLBACKDATA pData, const char *pcszUser, const char *pcszPassword)
190{
191 RT_NOREF(pData, pcszUser, pcszPassword);
192
193 RTPrintf("Authenticating user '%s' ...\n", pcszUser);
194
195 return VINF_SUCCESS;
196}
197
198static DECLCALLBACK(int) onUserDisonnect(PRTFTPCALLBACKDATA pData, const char *pcszUser)
199{
200 RT_NOREF(pData);
201
202 RTPrintf("User '%s' disconnected\n", pcszUser);
203
204 return VINF_SUCCESS;
205}
206
207static DECLCALLBACK(int) onFileOpen(PRTFTPCALLBACKDATA pData, const char *pcszPath, uint32_t fMode, void **ppvHandle)
208{
209 RT_NOREF(ppvHandle);
210
211 PFTPSERVERDATA pThis = (PFTPSERVERDATA)pData->pvUser;
212 Assert(pData->cbUser == sizeof(FTPSERVERDATA));
213
214 char *pszPathAbs = NULL;
215 if (RTStrAPrintf(&pszPathAbs, "%s/%s", pThis->szPathRootAbs, pcszPath) <= 0)
216 return VERR_NO_MEMORY;
217
218 int rc = RTFileOpen(&pThis->hFile, pszPathAbs, fMode);
219
220 RTStrFree(pszPathAbs);
221
222 return rc;
223}
224
225static DECLCALLBACK(int) onFileRead(PRTFTPCALLBACKDATA pData, void *pvHandle, void *pvBuf, size_t cbToRead, size_t *pcbRead)
226{
227 RT_NOREF(pvHandle);
228
229 PFTPSERVERDATA pThis = (PFTPSERVERDATA)pData->pvUser;
230 Assert(pData->cbUser == sizeof(FTPSERVERDATA));
231
232 return RTFileRead(pThis->hFile, pvBuf, cbToRead, pcbRead);
233}
234
235static DECLCALLBACK(int) onFileClose(PRTFTPCALLBACKDATA pData, void *pvHandle)
236{
237 RT_NOREF(pvHandle);
238
239 PFTPSERVERDATA pThis = (PFTPSERVERDATA)pData->pvUser;
240 Assert(pData->cbUser == sizeof(FTPSERVERDATA));
241
242 int rc = RTFileClose(pThis->hFile);
243 if (RT_SUCCESS(rc))
244 {
245 pThis->hFile = NIL_RTFILE;
246 }
247
248 return rc;
249}
250
251static DECLCALLBACK(int) onFileGetSize(PRTFTPCALLBACKDATA pData, const char *pcszPath, uint64_t *puSize)
252{
253 PFTPSERVERDATA pThis = (PFTPSERVERDATA)pData->pvUser;
254 Assert(pData->cbUser == sizeof(FTPSERVERDATA));
255
256 char *pszStat = NULL;
257 if (RTStrAPrintf(&pszStat, "%s/%s", pThis->szPathRootAbs, pcszPath) <= 0)
258 return VERR_NO_MEMORY;
259
260 RTPrintf("Retrieving file size for '%s' ...\n", pcszPath);
261
262 RTFILE hFile;
263 int rc = RTFileOpen(&hFile, pcszPath,
264 RTFILE_O_READ | RTFILE_O_OPEN | RTFILE_O_DENY_WRITE);
265 if (RT_SUCCESS(rc))
266 {
267 rc = RTFileQuerySize(hFile, puSize);
268 if (RT_SUCCESS(rc))
269 RTPrintf("File size is: %RU64\n", *puSize);
270 RTFileClose(hFile);
271 }
272
273 RTStrFree(pszStat);
274
275 return rc;
276}
277
278static DECLCALLBACK(int) onFileStat(PRTFTPCALLBACKDATA pData, const char *pcszPath, PRTFSOBJINFO pFsObjInfo)
279{
280 PFTPSERVERDATA pThis = (PFTPSERVERDATA)pData->pvUser;
281 Assert(pData->cbUser == sizeof(FTPSERVERDATA));
282
283 char *pszStat = NULL;
284 if (RTStrAPrintf(&pszStat, "%s/%s", pThis->szPathRootAbs, pcszPath) <= 0)
285 return VERR_NO_MEMORY;
286
287 RTPrintf("Stat for '%s'\n", pszStat);
288
289 RTFILE hFile;
290 int rc = RTFileOpen(&hFile, pszStat,
291 RTFILE_O_READ | RTFILE_O_OPEN | RTFILE_O_DENY_WRITE);
292 if (RT_SUCCESS(rc))
293 {
294 RTFSOBJINFO fsObjInfo;
295 rc = RTFileQueryInfo(hFile, &fsObjInfo, RTFSOBJATTRADD_NOTHING);
296 if (RT_SUCCESS(rc))
297 {
298 if (pFsObjInfo)
299 *pFsObjInfo = fsObjInfo;
300 }
301
302 RTFileClose(hFile);
303 }
304
305 RTStrFree(pszStat);
306
307 return rc;
308}
309
310static DECLCALLBACK(int) onPathSetCurrent(PRTFTPCALLBACKDATA pData, const char *pcszCWD)
311{
312 PFTPSERVERDATA pThis = (PFTPSERVERDATA)pData->pvUser;
313 Assert(pData->cbUser == sizeof(FTPSERVERDATA));
314
315 RTPrintf("Setting current directory to '%s'\n", pcszCWD);
316
317 /** @todo BUGBUG Santiy checks! */
318
319 return RTStrCopy(pThis->szCWD, sizeof(pThis->szCWD), pcszCWD);
320}
321
322static DECLCALLBACK(int) onPathGetCurrent(PRTFTPCALLBACKDATA pData, char *pszPWD, size_t cbPWD)
323{
324 PFTPSERVERDATA pThis = (PFTPSERVERDATA)pData->pvUser;
325 Assert(pData->cbUser == sizeof(FTPSERVERDATA));
326
327 RTPrintf("Current directory is: '%s'\n", pThis->szCWD);
328
329 return RTStrCopy(pszPWD, cbPWD, pThis->szCWD);
330}
331
332static DECLCALLBACK(int) onPathUp(PRTFTPCALLBACKDATA pData)
333{
334 RT_NOREF(pData);
335
336 return VINF_SUCCESS;
337}
338
339static DECLCALLBACK(int) onDirOpen(PRTFTPCALLBACKDATA pData, const char *pcszPath, void **ppvHandle)
340{
341 PFTPSERVERDATA pThis = (PFTPSERVERDATA)pData->pvUser;
342 Assert(pData->cbUser == sizeof(FTPSERVERDATA));
343
344 PFTPDIRHANDLE pHandle = (PFTPDIRHANDLE)RTMemAllocZ(sizeof(FTPDIRHANDLE));
345 if (!pHandle)
346 return VERR_NO_MEMORY;
347
348 /* Construct absolute path. */
349 char *pszPathAbs = NULL;
350 if (RTStrAPrintf(&pszPathAbs, "%s/%s", pThis->szPathRootAbs, pcszPath) <= 0)
351 return VERR_NO_MEMORY;
352
353 RTPrintf("Opening directory '%s'\n", pszPathAbs);
354
355 int rc = RTVfsChainOpenDir(pszPathAbs, 0 /*fFlags*/, &pHandle->hVfsDir, NULL /* poffError */, NULL /* pErrInfo */);
356 if (RT_SUCCESS(rc))
357 {
358 *ppvHandle = pHandle;
359 }
360 else
361 {
362 RTMemFree(pHandle);
363 }
364
365 RTStrFree(pszPathAbs);
366
367 return rc;
368}
369
370static DECLCALLBACK(int) onDirClose(PRTFTPCALLBACKDATA pData, void *pvHandle)
371{
372 RT_NOREF(pData);
373
374 PFTPDIRHANDLE pHandle = (PFTPDIRHANDLE)pvHandle;
375 AssertPtrReturn(pHandle, VERR_INVALID_POINTER);
376
377 RTVfsDirRelease(pHandle->hVfsDir);
378
379 RTMemFree(pHandle);
380 pHandle = NULL;
381
382 return VINF_SUCCESS;
383}
384
385static DECLCALLBACK(int) onDirRead(PRTFTPCALLBACKDATA pData, void *pvHandle, char **ppszEntry,
386 PRTFSOBJINFO pInfo, char **ppszOwner, char **ppszGroup, char **ppszTarget)
387{
388 RT_NOREF(pData);
389 RT_NOREF(ppszTarget); /* No symlinks yet */
390
391 PFTPDIRHANDLE pHandle = (PFTPDIRHANDLE)pvHandle;
392 AssertPtrReturn(pHandle, VERR_INVALID_POINTER);
393
394 size_t cbDirEntryAlloced = sizeof(RTDIRENTRYEX);
395 PRTDIRENTRYEX pDirEntry = (PRTDIRENTRYEX)RTMemTmpAlloc(cbDirEntryAlloced);
396 if (!pDirEntry)
397 return VERR_NO_MEMORY;
398
399 int rc;
400
401 for (;;)
402 {
403 size_t cbDirEntry = cbDirEntryAlloced;
404 rc = RTVfsDirReadEx(pHandle->hVfsDir, pDirEntry, &cbDirEntry, RTFSOBJATTRADD_UNIX);
405 if (RT_FAILURE(rc))
406 {
407 if (rc == VERR_BUFFER_OVERFLOW)
408 {
409 RTMemTmpFree(pDirEntry);
410 cbDirEntryAlloced = RT_ALIGN_Z(RT_MIN(cbDirEntry, cbDirEntryAlloced) + 64, 64);
411 pDirEntry = (PRTDIRENTRYEX)RTMemTmpAlloc(cbDirEntryAlloced);
412 if (pDirEntry)
413 continue;
414 }
415 else if (rc != VERR_NO_MORE_FILES)
416 break;
417 }
418
419 if (RT_SUCCESS(rc))
420 {
421 if (pDirEntry->Info.Attr.u.Unix.uid != NIL_RTUID)
422 {
423 RTFSOBJINFO OwnerInfo;
424 rc = RTVfsDirQueryPathInfo(pHandle->hVfsDir,
425 pDirEntry->szName, &OwnerInfo, RTFSOBJATTRADD_UNIX_OWNER, RTPATH_F_ON_LINK);
426 if ( RT_SUCCESS(rc)
427 && OwnerInfo.Attr.u.UnixOwner.szName[0])
428 {
429 *ppszOwner = RTStrDup(&OwnerInfo.Attr.u.UnixOwner.szName[0]);
430 if (!*ppszOwner)
431 rc = VERR_NO_MEMORY;
432 }
433 }
434
435 if ( RT_SUCCESS(rc)
436 && pDirEntry->Info.Attr.u.Unix.gid != NIL_RTGID)
437 {
438 RTFSOBJINFO GroupInfo;
439 rc = RTVfsDirQueryPathInfo(pHandle->hVfsDir,
440 pDirEntry->szName, &GroupInfo, RTFSOBJATTRADD_UNIX_GROUP, RTPATH_F_ON_LINK);
441 if ( RT_SUCCESS(rc)
442 && GroupInfo.Attr.u.UnixGroup.szName[0])
443 {
444 *ppszGroup = RTStrDup(&GroupInfo.Attr.u.UnixGroup.szName[0]);
445 if (!*ppszGroup)
446 rc = VERR_NO_MEMORY;
447 }
448 }
449 }
450
451 *ppszEntry = RTStrDup(pDirEntry->szName);
452 AssertPtrReturn(*ppszEntry, VERR_NO_MEMORY);
453
454 *pInfo = pDirEntry->Info;
455
456 break;
457
458 } /* for */
459
460 RTMemTmpFree(pDirEntry);
461 pDirEntry = NULL;
462
463 return rc;
464}
465
466int main(int argc, char **argv)
467{
468 int rc = RTR3InitExe(argc, &argv, 0);
469 if (RT_FAILURE(rc))
470 return RTMsgInitFailure(rc);
471
472 /* Use some sane defaults. */
473 char szAddress[64] = "localhost";
474 uint16_t uPort = 2121;
475
476 RT_ZERO(g_FTPServerData);
477
478 /*
479 * Parse arguments.
480 */
481 static const RTGETOPTDEF s_aOptions[] =
482 {
483 { "--address", 'a', RTGETOPT_REQ_IPV4ADDR }, /** @todo Use a string for DNS hostnames? */
484 /** @todo Implement IPv6 support? */
485 { "--port", 'p', RTGETOPT_REQ_UINT16 },
486 { "--root-dir", 'r', RTGETOPT_REQ_STRING },
487 { "--verbose", 'v', RTGETOPT_REQ_NOTHING }
488 };
489
490 RTEXITCODE rcExit = RTEXITCODE_SUCCESS;
491 unsigned uVerbosityLevel = 1;
492
493 RTGETOPTUNION ValueUnion;
494 RTGETOPTSTATE GetState;
495 RTGetOptInit(&GetState, argc, argv, s_aOptions, RT_ELEMENTS(s_aOptions), 1, RTGETOPTINIT_FLAGS_OPTS_FIRST);
496 while ((rc = RTGetOpt(&GetState, &ValueUnion)))
497 {
498 switch (rc)
499 {
500 case 'a':
501 RTStrPrintf2(szAddress, sizeof(szAddress), "%RU8.%RU8.%RU8.%RU8", /** @todo Improve this. */
502 ValueUnion.IPv4Addr.au8[0], ValueUnion.IPv4Addr.au8[1], ValueUnion.IPv4Addr.au8[2], ValueUnion.IPv4Addr.au8[3]);
503 break;
504
505 case 'p':
506 uPort = ValueUnion.u16;
507 break;
508
509 case 'r':
510 RTStrCopy(g_FTPServerData.szPathRootAbs, sizeof(g_FTPServerData.szPathRootAbs), ValueUnion.psz);
511 break;
512
513 case 'v':
514 uVerbosityLevel++;
515 break;
516
517 case 'h':
518 RTPrintf("Usage: %s [options]\n"
519 "\n"
520 "Options:\n"
521 " -a, --address (default: localhost)\n"
522 " Specifies the address to use for listening.\n"
523 " -p, --port (default: 2121)\n"
524 " Specifies the port to use for listening.\n"
525 " -r, --root-dir (default: current dir)\n"
526 " Specifies the root directory being served.\n"
527 " -v, --verbose\n"
528 " Controls the verbosity level.\n"
529 " -h, -?, --help\n"
530 " Display this help text and exit successfully.\n"
531 " -V, --version\n"
532 " Display the revision and exit successfully.\n"
533 , RTPathFilename(argv[0]));
534 return RTEXITCODE_SUCCESS;
535
536 case 'V':
537 RTPrintf("$Revision: 82842 $\n");
538 return RTEXITCODE_SUCCESS;
539
540 default:
541 return RTGetOptPrintError(rc, &ValueUnion);
542 }
543 }
544
545 if (!strlen(g_FTPServerData.szPathRootAbs))
546 {
547 /* By default use the current directory as serving root directory. */
548 rc = RTPathGetCurrent(g_FTPServerData.szPathRootAbs, sizeof(g_FTPServerData.szPathRootAbs));
549 if (RT_FAILURE(rc))
550 return RTMsgErrorExit(RTEXITCODE_FAILURE, "Retrieving current directory failed: %Rrc", rc);
551 }
552
553 /* Initialize CWD. */
554 RTStrPrintf2(g_FTPServerData.szCWD, sizeof(g_FTPServerData.szCWD), "/");
555
556 /* Install signal handler. */
557 rc = signalHandlerInstall();
558 if (RT_SUCCESS(rc))
559 {
560 /*
561 * Create the FTP server instance.
562 */
563 RTFTPSERVERCALLBACKS Callbacks;
564 RT_ZERO(Callbacks);
565
566 Callbacks.pfnOnUserConnect = onUserConnect;
567 Callbacks.pfnOnUserAuthenticate = onUserAuthenticate;
568 Callbacks.pfnOnUserDisconnect = onUserDisonnect;
569 Callbacks.pfnOnFileOpen = onFileOpen;
570 Callbacks.pfnOnFileRead = onFileRead;
571 Callbacks.pfnOnFileClose = onFileClose;
572 Callbacks.pfnOnFileGetSize = onFileGetSize;
573 Callbacks.pfnOnFileStat = onFileStat;
574 Callbacks.pfnOnPathSetCurrent = onPathSetCurrent;
575 Callbacks.pfnOnPathGetCurrent = onPathGetCurrent;
576 Callbacks.pfnOnPathUp = onPathUp;
577 Callbacks.pfnOnDirOpen = onDirOpen;
578 Callbacks.pfnOnDirClose = onDirClose;
579 Callbacks.pfnOnDirRead = onDirRead;
580
581 RTFTPSERVER hFTPServer;
582 rc = RTFtpServerCreate(&hFTPServer, szAddress, uPort, &Callbacks,
583 &g_FTPServerData, sizeof(g_FTPServerData));
584 if (RT_SUCCESS(rc))
585 {
586 RTPrintf("Starting FTP server at %s:%RU16 ...\n", szAddress, uPort);
587 RTPrintf("Root directory is '%s'\n", g_FTPServerData.szPathRootAbs);
588
589 RTPrintf("Running FTP server ...\n");
590
591 for (;;)
592 {
593 RTThreadSleep(200);
594
595 if (g_fCanceled)
596 break;
597 }
598
599 RTPrintf("Stopping FTP server ...\n");
600
601 int rc2 = RTFtpServerDestroy(hFTPServer);
602 if (RT_SUCCESS(rc))
603 rc = rc2;
604
605 RTPrintf("Stopped FTP server\n");
606 }
607 else
608 rcExit = RTMsgErrorExit(RTEXITCODE_FAILURE, "RTFTPServerCreate failed: %Rrc", rc);
609
610 int rc2 = signalHandlerUninstall();
611 if (RT_SUCCESS(rc))
612 rc = rc2;
613 }
614
615 /* Set rcExit on failure in case we forgot to do so before. */
616 if (RT_FAILURE(rc))
617 rcExit = RTEXITCODE_FAILURE;
618
619 return rcExit;
620}
621
注意: 瀏覽 TracBrowser 來幫助您使用儲存庫瀏覽器

© 2025 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette