Hi, So I've finally started thinking about adding support for file transfers via cliprdr channel. As noted by akallabeth [1], I also do not see any really portable way to implement it. There is no single standard on how to handle clipboard, even without supporting files, and MS-RDPECLIP [2] is fairly Windows-biased. However, I believe that the platform differences can be reasonably abstracted out from the implementation.
Akallabeth suggested the overall implementation strategy. To sum it up: 1) just read the requested files when cliprdr asks us for content, 2) use FUSE to provide files to local applications, backing the storage with cliprdr requests. I have given it a thought for some time, but have not delved too deep in the implementation yet. I would like to get some peer review to ensure that my understanding looks okay. Please note that I am mostly experienced with XFreeRDP on Linux as that's what I use. While I'm technically able to build and test all clients (except for DirectFB one), I have minor development experience with FreeRDP on other platforms. Please feel free to point out any possible portability issues which you may spot. [1]: https://github.com/FreeRDP/FreeRDP/issues/2424#issuecomment-281128986 [2]: https://msdn.microsoft.com/en-us/library/cc241066.aspx Background ---------- Here are the core points about cliprdr, file transfers, and related clipboard formats: 1. File content is transferred out of band. Regular clipboard contents are transferred via CLIPRDR_FORMAT_DATA_RESPONSE messages. However, for files these messages contain only metadata with the list of file names, sizes, flags, etc. The actual content of files is transferred via CLIPRDR_FILECONTENTS_RESPONSE messages. 2. File content is transferred asynchronously. CLIPRDR_FILECONTENTS_RESPONSE messages are sent in response to CLIPRDR_FILECONTENTS_REQUEST messages which are sent after format data has been received. Obviously, responses arrive separetely, with a certain delay. 3. Local applications expect clipboard data in timely manner. When a local application requests clipboard contents it expects the data to be available in a short time frame. For Windows and Mac we are required to synchronously provide some response. Selections on Linux/X11 work asynchronously, but if the data is not provided within a certain timeout (e.g., 10 seconds for GTK applications), the paste is canceled. For files this means that we must reasonably quickly provide the whole list of files which are expected to be immediately available for interaction via the filesystem. While such timeout may seem more than enough to download a single file, it has strong impact on large files, large amout of small files, and complex directory structure. Thus it may be impractical to delay providing the file list data until the file are downloaded from the server. There are little issues with local-to-remote transfers as we simply need to fullfill cliprdr requests. However, for remote-to-local transfers we will have to ensure that the platform can provide a way to publish a list of files without actually having their content. - On Windows files are pasted as "FileGroupDescriptorW" format which uses some black COM magic to provide file content in a streamed fashion (see FreeRDP's Windows client for example). - On Mac NSPasteboard supports NSFilesPromisePboardType format which allows an application to immediately paste a list of files while promising to provide their content later. - On Linux we have freedom (i.e., lack of any standard way to transfer files via clipboard). Known conventions include MIME types text/uri-list, x-special/gnome-copied-files, and application/x-kde-cutselection which all are essentially text lists of URIs describing the locations of the pasted files. The content of files is expected to be immediately available, any deferred retrieval should be handled by the filesystem. Gnome applications use GVfs, KDE applications use KIOSlaves, POSIX applications can use FUSE. Unfortunately, GVfs and KIOSlaves are baked into respective desktop environments and are not really extendable which leaves us with only FUSE. Proposed architecture --------------------- ## Cast of the play There are three principal participants in clipboard redirection (using XFreeRDP as an example): - cliprdrPlugin (channels/cliprdr/client) * implements MS-RDPECLIP protocol * parses and serializes messages * provides a callback interface to the client - xfClipboard (client/X11/xf_cliprdr) * interacts with the local system clipboard (X Selection), implements some necessary quirks * implements and uses cliprdrPlugin callbacks for receiving and sending format data when requested by local or remote clipboard * uses wClipboard for actual format conversion - wClipboard (winpr/libwinpr/clipboard) * stores format data * provides conversion between formats Fundamental roles of these objects are unchanged with addition of file transfers, but some extensions and special cases will have to be made. ## cliprdrPlugin cliprdrPlugin will still be tasked with parsing and serializing protocol messages, there should be minor architectural changes to it. It already supports file clipping messages, only some flag definitions are missing. Another thing that is missing is support for CLIPRDR_FILELIST parsing. We should add some common functions to avoid duplicating parsers and serializers in clients. These utility functions may be exported by <freerdp/client/cliprdr.h> with the following approximate signatures: /** * Parse a packed file list. * * `fileList->fileDescriptorArray` is updated in case of successful * parsing. The pointer is not freed by this function, so the caller * must ensure that its contents can be safely overwritten. * The resulting array must be freed with the `free()` function. * * @param [in] formatData packed file list to parse. * @param [in] formatDataLength length of formatData in bytes. * @param [out] fileList parsed file list stored here. * * @returns 0 on success, otherwise a Win32 error code. */ UINT cliprdr_parse_file_list(const BYTE* formatData, UINT32 formatDataLength, CLIPRDR_FILELIST* fileList); /** * Serialize a packed file list. * * The resulting `formatData` must be freed with the `free()` * function. * * @param [in] fileList file list to serialize. * @param [out] formatData packet file list stored here. * @param [out] formatDataLength length of formatData stored here. * * @returns 0 on success, otherwise a Win32 error code. */ UINT cliprdr_serialize_file_list(const CLIPRDR_FILELIST* fileList, BYTE** formatData, UINT32* formatDataLength); ## xfClipboard xfClipboard (or any other cliprdr client) should also stay relatively unchanged. It is primarily tasked with correctly putting and extracting converted format data from and into the local clipboard. The client is a mediator between cliprdrPlugin, wClipboard, and the local system clipboard. File lists are just another format that it needs to handle. However, this format is a bit special as file lists must outlive the handling of CLIPRDR_FORMAT_DATA_{RESPONSE,REQUEST} messages. Here are the transfer sequences to clarify the idea of the implementation. ### Remote-to-local transfer sequence 1. The user copies some files in the remote session. 2. The server sends us a CLIPRDR_FORMAT_LIST with "FileGroupDescriptorW" among the available formats. 3. The client receives the format list, converts remote formats to local equivalents, and announces the available formats on the local clipboard. 4. The user pastes the files into some local application. The application requests a file format from the client. 5. The client handles the request and sends an appropriate CLIPRDR_FORMAT_DATA_REQUEST to the server. 6. The server replies with CLIPRDR_FORMAT_DATA_RESPONSE containing the list of files. The client receives and parses the message. 7. The client parses the packed file list into CLIPRDR_FILELIST and stores it in the wClipboard. Note that wClipboard resides in WinPR which should not know about FreeRDP's cliprdr, but it may know about Windows clipboard format structures. Thus we will need to pass the files as new FILEDESCRIPTOR structures (identical to CLIPRDR_FILEDESCRIPTOR). 8. wClipboard looks through the file list and starts providing it to the local file system. (It does not have any content yet.) 9. The client asks wClipboard to convert the file list into some local clipboard format (e.g., text/uri-list). 10. The client provides the resulting format data to that local application which has been waiting for it since step 4. 11. The local application tries to open and read a file which it has received. The filesystem magically transfers control to the client's wClipboard, requesting a chunk of data. 12. wClipboard handles the request and forwards it to the client. 13. The client sends a CLIPRDR_FILECONTENTS_REQUEST to the server. 14. The server fetches the data and replies with a CLIPRDR_FILECONTENTS_RESPONSE. 15. The client receives the response and forwards the data to wClipboard. 16. wClipboard forwards the data to local application via some filesystem magic. 17. Steps 11--16 are repeated until the local application is satisified, or until the server sends a new format list which means that the clipboard content is cleared and file content is no more available (unless file locking is used). ### Local-to-remote transfer sequence 1. The user copies some files in a local application. The application announces a local file format on the local clipboard. 2. The client receives a notification, converts the local formats to remote equivalents (with "FileGroupDescriptorW" among them), and sends a CLIPRDR_FORMAT_LIST to the server. 3. The user pastes the files in the remote session. 4. The remote desktop handles the paste and the server sends an appropriate CLIPRDR_FORMAT_DATA_REQUEST to the client. The client receives and parses the message. 5. The client requests a local file format from the local application and waits for the data to be available. 6. The client passes the local file format data to wClipboard which parses it, stores it, and prepares to serve the content requests. 7. The client asks wClipboard to convert the local file list into a list of FILEDESCRIPTOR structures. 8. The client converts the list of FILEDESCRIPTOR structures into a serialized CLIPRDR_FILELIST and sends a CLIPRDR_FORMAT_DATA_RESPONSE to the server. 9. The remote desktop needs a chunk of file data so the server sends a CLIPRDR_FILECONTENTS_REQUEST to the client. The client receives and parses the message. 10. The client asks wClipboard to provide a chunk of data from the specified file. 11. wClipboard fetches the data from the local filesystem. 12. wClipboard forwards the data to the client. 13. The client replies with CLIPRDR_FILECONTENTS_RESPONSE to the server. 14. The steps 9--13 are repeated until the remote desktop is satisified, or until the client sends a new format list to the server (which clears the file list stored in wClipboard). ## wClipboard wClipboard looks like an ideal place for the file clipping support. Effectively, it implements interface and behavior of Windows clipboard for non-Windows platforms. The way Windows handles files on clipboard seems to fit exactly into the intended purpose of this object, so I believe that we should put the portable implementation there. However, WinAPI does not provide any clipboard functions specifically for handling files. Moreover, the functions and their interface will be somewhat biased towards cliprdr needs. But I believe that their utility justifies placing them into WinPR. ### wClipboard interface extension The transfer sequences described above require wClipboard to have the following abilities: - list supported local file list formats - convert between remote file lists and local format data - publish remote files to local filesystem in an on-demand fashion - fetch content of files published on local clipboard There are multiple local file list formats which may have different dependencies, so wClipboard should be extendable to handle them. Their availability may depend on how libfreerdp and libwinpr are built, so clients should dynamically adjust the formats that they may use to interact with local clipboard, without hardcoding them. There are also multiple ways to publish remote files into local filesystem so that they are available for clipboard transfers. Every platform has its own preferred way of doing this. Platform interfaces for this are usually asynchronous. Thus it makes sense to develop some common facade interface for wClipboard and implement platform-specific parts in subsystems (like it is done, for example, with rdpsnd). The specific interface implementation will need to be able to issue cliprdr requests and receive the responses. WinPR should not depend on FreeRDP or on a specific FreeRDP client, so we will need a delegate interface to provide the ability to fetch file content via cliprdr. Finally, files on local clipboard not always refer to files located on the local disk drive. Depending on the actual clipboard format it may contain references to COM objects, URLs of remote resources, etc. They may be not accessible via simple open() or fopen() calls, requiring to use some other API instead (which may be asynchronous or synchronous). Thus it makes sense to hide this aspect behind an interface as well. ### Example of an asynchronous interface Let's take as an example the task of providing remote files in local filesystem. For this we will need to tell the local clipboard subsystem about available files and then fulfill its data requests. The file list can be published by something like this: /** * Provide remote files on local clipboard. * * This function copies the list of files so it can be freed after * the call. * * @param clipboard clipboard instance. * @param files the list of remote files to provide. * @param fileCount number of `files`. * * @returns 0 on success, otherwise a Win32 error code. */ UINT ClipboardSetRemoteFileData(wClipboard* clipboard, const FILEDESCRIPTOR* files, UINT32 fileCount); This function is a special case of ClipboardSetData() for file lists. It will copy the file list into wClipboard, increment the sequence number, and provide the list to local clipboard subsystem so that it can make arrangements for the files to become available on the local clipboard. (Sister function ClipboardGetRemoteFileData() may be added as well.) The local clipboard subsystem will need to ask the client to perform cliprdr requests. It may do so via the following delegate interface. Note that it is asynchronous, requests and replies carry on the full context as well as the sequence number to track outdated requests. /** * Backing storage of remote files. * * `clipboard` is a backlink to corrensponding wClipboard instance. * * `custom` is an opaque client context which may be used by * client callbacks. * * `Clipboard*` callbacks should be filled in by the client, they * will be used by wClipboard to request content and information * about remote files. * * `Client*` callbacks are filled by wClipboard, they should be * used by the client to report completion of matching wClipboard * requests. */ struct wClipboardRemoteFileDelegate { wClipboard* clipboard; void* custom; /** * Request the client to retrieve the size of a remote file. * * @param delegate delegate instance. * @param sequence current sequence number. * @param fileIndex index of the file. * * @returns 0 on success, otherwise a Win32 error code. */ UINT (*ClipboardRequestFileSize) (wClipboardRemoteFileDelegate* delegate, UINT32 sequence, UINT32 fileIndex); /** * Provide the size of a remote file to clipboard. * * @param delegate delegate instance. * @param sequence sequence number of the request. * @param fileIndex index of the file. * @param fileSize size of the file in bytes. * * @returns 0 on success, otherwise a Win32 error code. */ UINT (*ClientProvideFileSize) (wClipboardRemoteFileDelegate* delegate, UINT32 sequence, UINT32 fileIndex, UINT64 fileSize); UINT (*ClipboardRequestFileContent) (wClipboardRemoteFileDelegate* delegate, UINT32 sequence, UINT32 fileIndex, UINT64 position, UINT32 requestedBytes); UINT (*ClientProvideFileContent) (wClipboardRemoteFileDelegate* delegate, UINT32 sequence, UINT32 fileIndex, UINT64 position, const BYTE* data, UINT32 dataLength); /* . . . */ }; /** * Set the current remote file delegate. * * Installs the provided delegate into the clipboard and fills in * clipboard-side callbacks in it. * * The clipboard does not take ownership of the delegate. It should * be freed manually before calling ClipboardDestroy(). * * @param clipboard clipboard instance. * @param delegate delegate instance. * * @returns 0 on success, otherwise a Win32 error code. */ UINT ClipboardSetRemoteFileDelegate(wClipboard* clipboard, wClipboardRemoteFileDelegate* delegate); ### Further details on wClipboard I did not think it through too deeply as the actual implementation is likely to be different from what can be seen here, but you should get the whole idea from this. The interface has to be asynchronous as it's the most natural way to do input-output for backends (be it cliprdr requests, FUSE requests, or GIO requests). Unfortunately, asynchronicity is akin to cancer, it tends to spread out. The actual clipboard subsystems will have a similar interface for wClipboard. I suppose that we will initially need at least the following ones: - dumb remote backend: * the most portable one * initially returns a 'not available' error * downloads the files into a temporary directory in background * when download is complete it can provide the files to local clipboard * if clipboard content is changed then the download is canceled and the client gets an error response - FUSE remote backend: * requires libfuse to be available * immediately makes the files available in a temporary FUSE-mapped directory * downloads file content on demand - text/uri-list local backend: * supports file:// schema in text/uri-list MIME format * opens and reads local files using standard C or POSIX The subsystems encapsulate the platform differences. Adding a new backend in WinPR should automagically add its support to all clients which use cliprdr for file transfers without any additional changes. Issues to consider ------------------ ## Concurrency and asynchronicity wClipboard will have to have asynchronous interface to accomodate the fact that data requests can take time to complete. It is also possible for asynchronous requests to silently fail, be abandoned without further notice, be duplicated if they take too long to complete, or be completed long after useful timeframe (e.g., after clipboard content has changed). We should be ready for all this. Interactions will also most likely involve more than one thread, thus care must be taken to correctly program the concurrent parts of the implementation, using queues, locks, reference counts, etc. ## Latency and throughput Obviously, this is hardly a lightning fast approach to transferring file content. I consider this mostly a non-issue as this is not an intended use case of clipboard redirection, which is meant to be a _convenient_ way of transferring files. When speed matters and for bulky transfers one should arguably use disk drive redirection directly. If this becomes an issue then we can leverage disk drive redirection as a temporary storage for pasted files. Cliprdr protocol provides support for this in the form of CLIPRDR_TEMP_DIRECTORY message used to negotiate a temporary storage location which may be used to avoid file transfer via CLIPRDR_FILECONTENTS_{REQUEST,RESPONSE} protocol. ## Caching Both local-to-remote and remote-to-local transfers can benefit from some amount of caching. For local-to-remote transfers file content caching should be performed by the remote side, but from our local side it would be wise to avoid reopening local files for each server request, keeping them open for as long as possible. We can also benefit from the fact that the server usually issues content requests in sequential way so we may avoid unnecessary seeking. For remote-to-local transfers it would be wise to cache the received file content so as to avoid performing unnecessary duplicate requests when files are pasted or read multiple times by the local application. ## File locking Cliprdr protocol assumes that the file content is available only until a new format list is published by either client or server. Files may take a significant amount of time to transfer so it's not unreasonable to expect clipboard content to change during the transfer. To allow the transfer to complete regardless of content changes cliprdr provides a file locking facility. It allows the peer to pin requested files so that their content may be queried even after clipboard content change. Most likely we will have to implement both usage of the locking protocol for remote-to-local transfer and support for the protocol for local-to-remote transfers. Otherwise it may be extemely easy for the user to accidentally cancel an ongoing file transfer. ## Intersession transfers Aside from local-to-remote and remote-to-local transfers there also exists a case when files are transferred between two remote sessions. Currently this use case is optimized in XFreeRDP via CF_RAW format which directly transfers raw data between remote sessions without doing any intermediate format conversions, effectively proxying CLIPRDR_FORMAT_DATA_REQUESTS and CLIPRDR_FORMAT_DATA_RESPONSES. Unfortunately, raw data transfers will not work as is for files. One option is to implement some similar proxying for CLIPRDR_FILECONTENTS_{REQUEST,RESPONSE} messages. Another one is to simply disable raw transfer capability when files are available, thus treating another remote session as any other application. Not using raw transfers for files is quick to implement, but may lack performance. Proxying requests may provide better performance, but will need some proper interprocess communication mechanism to be implemented (though, X window properties may work fine). Implementation plan ------------------- Well, I'm not at my software development day job, so there obviously won't be any schedule commitment. I also dislike grand half-a-year detailed plans which never work out as expected. I believe the outline above will suffice as a roadmap. Thus I propose to move in the following small and vague steps, with testing, bugfix, and improving the code in between them: 1. Implement transferring local files to remote desktop for XFreeRDP. 2. Implement transferring remote files to local desktop for XFreeRDP. 3. The users should become happier now. 4. Later implement file locking, add support for other clients, improve intersession transfers, add drag-n-drop support, use temporary directories, add CF_HDROP support for Windows XP, and do other fancies to make the users even more happy. Parting words ------------- So, how does this all look to you? Will it fly? Please feel free to comment on the design. While this looks fairly portable and extendable to me, I would like to get a second opinion. Especially given that I intend this to be the groundwork for supporting file transfers in non-X11 clients (with which I have close to zero development experience). I am going to start writing the actual code in the nearest time. If you are interested then you can peek at it in my GitHub fork [3]. That pull request is there just to keep my notes around, I will file proper pull requests to FreeRDP repo as soon as there is something reviewable. If you have any questions, suggestions, confenssions, threats, etc., I will also available on the IRC (as ilammy). [3]: https://github.com/ilammy/FreeRDP/pull/4 ------------------------------------------------------------------------------ Check out the vibrant tech community on one of the world's most engaging tech sites, SlashDot.org! http://sdm.link/slashdot _______________________________________________ FreeRDP-devel mailing list FreeRDP-devel@lists.sourceforge.net https://lists.sourceforge.net/lists/listinfo/freerdp-devel