{"id":51043561,"url":"https://github.com/qoretechnologies/module-ssh","last_synced_at":"2026-06-22T12:01:46.938Z","repository":{"id":349610528,"uuid":"1203060272","full_name":"qoretechnologies/module-ssh","owner":"qoretechnologies","description":"SSH server and SFTP virtualization module for Qore","archived":false,"fork":false,"pushed_at":"2026-05-02T17:56:29.000Z","size":593,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"develop","last_synced_at":"2026-05-02T19:28:25.269Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://qore.org","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/qoretechnologies.png","metadata":{"files":{"readme":"README","changelog":"NEWS","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-06T17:21:49.000Z","updated_at":"2026-05-02T17:56:33.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/qoretechnologies/module-ssh","commit_stats":null,"previous_names":["qoretechnologies/module-ssh"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/qoretechnologies/module-ssh","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qoretechnologies%2Fmodule-ssh","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qoretechnologies%2Fmodule-ssh/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qoretechnologies%2Fmodule-ssh/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qoretechnologies%2Fmodule-ssh/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/qoretechnologies","download_url":"https://codeload.github.com/qoretechnologies/module-ssh/tar.gz/refs/heads/develop","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qoretechnologies%2Fmodule-ssh/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34647750,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-22T02:00:06.391Z","response_time":106,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-22T12:01:45.917Z","updated_at":"2026-06-22T12:01:46.924Z","avatar_url":"https://github.com/qoretechnologies.png","language":"C++","funding_links":[],"categories":[],"sub_categories":[],"readme":"Qore ssh module\n\nINTRODUCTION\n------------\nThis repository contains a new Qore SSH module based on libssh.\n\nUnlike the existing ssh2 module, this module is intended primarily for\nserver-side SSH and SFTP use cases with controlled command processing,\nauditable file transfer workflows, and generic integration scenarios.\n\nThis module requires Qore 2.3 or newer and the `sshutil` binary module. The\n`sshutil` module provides shared abstract SSH provider interfaces used by the\nserver host-key provider API.\n\nSTATUS\n------\nThis repository now includes working SSH/SFTP server foundations plus generic\nvirtual filesystem helpers for SFTP testing and backend virtualization.\n\nThe design principles are:\n- consistent CamelCase naming for public classes and namespaces\n- server-side SSH and SFTP focus\n- local configuration and multi-listener support\n- pluggable authentication, sandboxing, and instrumentation\n- secure and auditable actions\n- generic command and file integration APIs\n\nThe current API shape is documented in:\n    design/api-spec.md\n\nThe focused generic ConnectionProvider/DataProvider design note is:\n    design/data-provider-infrastructure.md\n\nBoundary note:\n- generic provider infrastructure belongs in this repo\n- application-specific adapters and workflows do not\n\nGENERIC CONNECTIONPROVIDER MODULE\n---------------------------------\nThe repo now includes a real `SshServerConnections` qlib module for generic\n`ConnectionProvider`-style local server definitions.\n\nCurrent connection classes:\n- `SshServerConnections::AbstractSshServerConnection`\n- `SshServerConnections::SshCommandServerConnection`\n- `SshServerConnections::SftpServerConnection`\n\nThe command side now also has a generic virtualization helper:\n- `SshServerCommandProvider::VirtualSshCommandService`\n\nCommand-side boundary:\n- command virtualization is exposed through `SshCommandServerConnection` plus\n  `VirtualSshCommandService`\n- no generic command `DataProvider` is provided in this repo\n- `DataProvider` support is reserved for the SFTP/file-exchange side where the\n  abstraction is a natural fit\n\nTyped command contract:\n- request: `Qore::Ssh::SshCommandRequest`\n  - stable fields include `session_id`, `server_name`, `listener_name`,\n    `username`, `principal`, `command`, `arguments`, `environment`, and\n    `metadata`\n  - `metadata.request_type` distinguishes `exec` from `subsystem`\n  - `metadata.subsystem` carries the subsystem name when applicable\n- result: `Qore::Ssh::SshCommandResult`\n  - stable fields include `accepted`, `exit_status`, `message`, `stdout`,\n    `stderr`, and `result_data`\n  - higher-level integration payloads should go in `result_data`\n\nVirtual command-service behavior:\n- all policy and audit controls are optional; basic usage remains\n  `VirtualSshCommandService service();`\n- supports static and closure-backed exec handlers\n- supports static and closure-backed subsystem handlers\n- supports exact-name allow/deny policy for commands and subsystems\n- supports principal-aware policy overrides with `setPrincipalPolicy()`\n- request capture can be bounded with `request_history_limit`\n- `request_history_limit = 0` disables capture and clears retained history\n- `capture_environment = False` redacts `environment` from captured requests\n- `capture_arguments = False` omits structured `arguments` from captured requests\n- `capture_identity = False` omits `username` and `principal` from captured requests\n- `captured_string_limit` truncates captured audit strings without affecting live\n  command dispatch\n- `max_concurrent_requests` optionally caps active virtual command dispatches\n- `max_concurrent_requests_per_principal` optionally caps active virtual command\n  dispatches per authenticated principal\n- policy-denied requests return `exit_status = 126` with reason codes:\n  - `command-policy-denied`\n  - `subsystem-policy-denied`\n  - `command-concurrency-above-maximum`\n  - `principal-command-concurrency-above-maximum`\n- advanced command-side features are exposed through typed\n  `VirtualSshCommandServiceOptions` options and runtime setters rather than\n  `DataProvider` action options\n\nCurrent registered schemes:\n- `sshsrv`\n- `sshserver`\n- `sftpsrv`\n- `sftpserver`\n\nThese connections return live `Qore::Ssh::SshServer` objects. Calling\n`get(True)` starts the server immediately. The connection URL defines the bind\naddress and port for a single listener, and the connection options project to\ntyped `Qore::Ssh::SshServerConfig` / `Qore::Ssh::SshListenerConfig` hashes via:\n- `getServerConfig()`\n- `getListenerConfig()`\n\nListener-level operational controls include:\n- grouped `limits.max_sessions` for a deterministic cap on concurrently\n  accepted sessions per listener\n- grouped `services.enabled_auth_methods`,\n  `services.allow_command_service`, and `services.allow_sftp_service`\n- grouped `auth` for config-driven authentication, or `auth_provider` for\n  user-defined provider objects implementing\n  `authenticate(hash\u003cQore::Ssh::SshAuthContextInfo\u003e)`\n\nLogging note:\n- connection helpers, the built-in auth provider, the virtual command service,\n  and the virtual SFTP helper/exchange objects all accept\n  `Qore::Logger::LoggerInterface`\n- accepted auth and command outcomes log at `info`\n- rejected auth and command outcomes log at `warn`\n- detailed auth attempts and virtual filesystem churn log at `detail`\n- configuration-oriented command-service messages log at `debug`\n- event-listener callback failures log at `error`\n\nMinimal example:\n\n```qore\nSshServerConnections::SftpServerConnection conn({\n    \"name\": \"local-sftp\",\n    \"url\": \"sftpsrv://127.0.0.1:0\",\n    \"opts\": {\n        \"host_keys\": (\"/path/to/ssh_host_ed25519_key\",),\n        \"limits\": \u003cSshServerConnections::SshServerConnectionLimits\u003e{\n            \"max_sessions\": 8,\n        },\n        \"services\": \u003cSshServerConnections::SshServerConnectionServices\u003e{\n            \"enabled_auth_methods\": (\"password\",),\n            \"allow_sftp_service\": True,\n            \"allow_command_service\": False,\n        },\n        \"auth\": \u003cSshServerAuthProvider::VirtualSshAuthProviderOptions\u003e{\n            \"methods\": (\"password\",),\n            \"password\": \u003cSshServerAuthProvider::VirtualSshPasswordAuthOptions\u003e{\n                \"users\": (\n                    \u003cSshServerAuthProvider::VirtualSshPasswordUserInfo\u003e{\n                        \"username\": \"demo\",\n                        \"password\": \"secret\",\n                        \"decision\": \u003cSshServerAuthProvider::VirtualSshAuthDecisionTemplate\u003e{\n                            \"principal\": \"principal:demo\",\n                        },\n                    },\n                ),\n            },\n        },\n    },\n});\n\nQore::Ssh::SshServer server = cast\u003cQore::Ssh::SshServer\u003e(conn.get());\nhash\u003cQore::Ssh::SshServerStatus\u003e status = server.getStatus();\nserver.stop();\n```\n\nVirtual command example:\n\n```qore\nSshServerCommandProvider::VirtualSshCommandService service();\nservice.addTextCommand(\"status\", \"ok\\n\", \"warn\\n\", 23, True, {\"mode\": \"virtual\"});\n\nSshServerConnections::SshCommandServerConnection conn({\n    \"name\": \"local-ssh-command\",\n    \"url\": \"sshserver://127.0.0.1:0\",\n    \"opts\": {\n        \"host_keys\": (\"/path/to/ssh_host_ed25519_key\",),\n        \"command_backend\": service,\n    },\n});\n```\n\nAdvanced policy/audit options remain optional:\n\n```qore\nSshServerCommandProvider::VirtualSshCommandService service(\n    \u003cSshServerCommandProvider::VirtualSshCommandServiceOptions\u003e{\n        \"policy\": \u003cSshServerCommandProvider::VirtualSshCommandPolicy\u003e{\n            \"allow_all_commands\": True,\n            \"allow_all_subsystems\": True,\n        },\n        \"audit\": \u003cSshServerCommandProvider::VirtualSshCommandAuditOptions\u003e{\n            \"request_history_limit\": 2,\n            \"capture_environment\": False,\n            \"capture_arguments\": True,\n            \"capture_identity\": True,\n            \"captured_string_limit\": 64,\n        },\n        \"limits\": \u003cSshServerCommandProvider::VirtualSshCommandLimits\u003e{\n            \"max_concurrent_requests\": 8,\n            \"max_concurrent_requests_per_principal\": 1,\n        },\n    }\n);\nservice.setPrincipalPolicy(\"principal:nobody\", \u003cSshServerCommandProvider::VirtualSshCommandPrincipalPolicy\u003e{\n    \"allow_all_commands\": False,\n    \"allow_all_subsystems\": False,\n    \"allowed_commands\": (\"status\",),\n    \"allowed_subsystems\": (\"metrics\",),\n});\nservice.addTextCommand(\"status\", \"ok\\n\", \"warn\\n\", 23, True, {\"mode\": \"virtual\"});\nservice.addTextSubsystem(\"metrics\", \"metrics:ok\\n\", NOTHING, 0, True, {\"mode\": \"subsystem\"});\nservice.setAuditOptions(\u003cSshServerCommandProvider::VirtualSshCommandAuditOptions\u003e{\n    \"capture_arguments\": False,\n    \"captured_string_limit\": 64,\n});\n```\n\nThe grouped `policy`, `audit`, and `limits` hashes are the recommended\nuser-friendly surface for advanced controls. Individual runtime setters remain\navailable for direct targeted changes.\nThe grouped runtime setters replace their whole domain deterministically:\n- `setPolicy()` replaces command/subsystem allow/deny state\n- `setAuditOptions()` replaces retained-audit capture settings\n- `setLimits()` replaces active-dispatch concurrency caps\n- `clearPolicy()`, `clearAuditOptions()`, and `clearLimits()` restore defaults\n- `getPolicy()`, `getAuditOptions()`, and `getLimits()` return typed snapshots\n  for inspection or higher-level configuration mapping\n\nA runnable command-side example is also available in:\n- `examples/VirtualSshCommandServer.qr`\n- `examples/VirtualSshCommandLiveServer.qr`\n\nLocal preflight:\n\n```bash\ntest/preflight.sh\n```\n\nThis runs the local build, the core qtest sweep, the live examples, and the\ndocs build against `QORE_MODULE_DIR=build`.\n\nGENERIC VIRTUAL SFTP HELPERS\n----------------------------\nThe repo includes generic qlib helpers for virtual SFTP backends:\n- `SftpServerDataProvider::VirtualSftpFilesystem`\n- `SftpServerDataProvider::VirtualSftpExchange`\n- `SftpServerDataProvider::VirtualSftpExchangeRecordStore`\n\nThese helpers are generic and test-focused. They do not contain\napplication-specific logic.\n\nThe helper layer now exposes strong typing for its user-facing results:\n- `SftpServerDataProvider::VirtualSftpFilesystemOptions`\n- `SftpServerDataProvider::VirtualSftpExchangeOptions`\n- `SftpServerDataProvider::VirtualSftpPathRequest`\n- `SftpServerDataProvider::VirtualSftpSubmissionRequest`\n- `SftpServerDataProvider::VirtualSftpCompletionRequest`\n- `SftpServerDataProvider::VirtualSftpRecordRequest`\n- `SftpServerDataProvider::VirtualSftpRecordSubmissionRequest`\n- `SftpServerDataProvider::VirtualSftpRecordCompletionRequest`\n- `SftpServerDataProvider::VirtualSftpInboundPolicy`\n- `SftpServerDataProvider::VirtualSftpFileSnapshot`\n- `SftpServerDataProvider::VirtualSftpSnapshot`\n- `SftpServerDataProvider::VirtualSftpEntryInfo`\n- `SftpServerDataProvider::VirtualSftpRecordInfo`\n\nThis gives callers stable typed shapes for constructor options, helper\nrequests, helper results, backend request/result overloads, snapshots,\nlistings, and record-style queue operations, while still allowing\nintentionally dynamic backend objects where that flexibility is needed.\n\nBuilt-in helper-managed flags are now also projected into a typed\n`VirtualSftpBuiltinState` on snapshot/stat/listing results. That covers stable\nstate such as archive/claim markers and exchange roles, while `metadata`\nremains available for application-defined payloads like `kind`.\n\nFor exchange completion-style operations, `VirtualSftpRecordInfo.path` may be\n`NOTHING` for delete results where no final path remains.\n\nRecommended usage is to prefer the typed request hashes even for helper calls.\nThe virtual filesystem and exchange backends now also expose typed overloads\nfor the backend verbs using `Qore::Ssh::SftpPathRequest`,\n`Qore::Ssh::SftpOpenRequest`, `Qore::Ssh::SftpRenameRequest`,\n`Qore::Ssh::SftpReadRequest`, `Qore::Ssh::SftpReadResult`,\n`Qore::Ssh::SftpWriteRequest`, `Qore::Ssh::SftpCloseRequest`,\n`Qore::Ssh::SftpPathInfo`, `Qore::Ssh::SftpListResult`,\n`Qore::Ssh::SftpOpenResult`, and `Qore::Ssh::SftpOperationResult`.\n`SftpSession` now dispatches typed request hashes for both low-level\nfilesystem verbs and streamed handle verbs. Lower-level dynamic backend maps\nare still available for intentionally dynamic backends, but the typed hashes\nare the primary public surface described here.\nThe high-level `SftpSession` convenience helpers also support typed request\noverloads now, so callers can stay in the typed API for:\n- `submitData()`\n- `submitLocalFile()`\n- `retrieveData()`\n- `completeRetrieveDelete()`\n- `completeRetrieveClaim()`\n- `completeRetrieveArchive()`\n- `retrieveToLocalFile()`\n\nFor retrieval-completion flows, use the explicit typed request field\n`completion_mode`.\n\n`VirtualSftpExchangeOptions` also supports a typed `inbound_policy` for\naccepting or rejecting inbound uploads. Current built-in policy fields are:\n- `accept_filename_glob`\n- `reject_filename_glob`\n- `accept_filename_regex`\n- `reject_filename_regex`\n- `min_size`\n- `max_size`\n- `max_file_count`\n- `max_total_bytes`\n- `max_concurrent_uploads`\n- `allow_overwrite`\n- `reject_duplicate_name`\n- `reject_duplicate_size`\n- `reject_duplicate_hash`\n- `accept_content_prefix`\n- `reject_content_prefix`\n- `accept_content_type_hints`\n- `reject_content_type_hints`\n- `validation_callback`\n- `allow_hidden_files`\n- `allow_principals`\n- `deny_principals`\n- `principal_policies`\n- `principal_routes`\n- `directory_policies`\n\nThese policy checks are enforced consistently for helper submissions,\n`DataProvider` `submit-inbound` actions, and real SFTP uploads handled through\n`VirtualSftpExchange`.\n\nFor arbitrary content validation, set `validation_callback` to a typed closure\nreceiving `hash\u003cSftpServerDataProvider::VirtualSftpInboundValidationRequest\u003e`\nand returning `hash\u003cSftpServerDataProvider::VirtualSftpInboundValidationResult\u003e`.\nNormal callback rejections should return `accepted: False` with `reason_code`\nand `reason_desc`; callback exceptions are treated as real backend errors.\n\n`VirtualSftpExchangeOptions` also supports typed retention policies:\n- `inbound_retention_policy`\n- `archive_retention_policy`\n- `claim_retention_policy`\n\nEach retention policy currently supports:\n- `max_age_seconds`\n\nAccepted inbound checksum generation is optional through\n`generate_inbound_checksum`. When enabled, accepted inbound submit results and\nevents carry a typed SHA-256 checksum, and that checksum is also persisted in\nvirtual file metadata so later stat/list/retrieve flows can surface it without\nrecomputing it.\n\nWhen configured, accepted inbound files and archive/claim completion targets\nare stamped with typed retention metadata including `retention_queue`,\n`retained_at`, and `expires_at`. Expired files can then be removed\ndeterministically with `VirtualSftpExchange::cleanupExpired()`.\n\nDuplicate rejection uses typed reason codes in rejected inbound events:\n- `duplicate-name-rejected`\n- `duplicate-size-rejected`\n- `duplicate-hash-rejected`\n- `content-prefix-rejected`\n- `content-type-hint-rejected`\n- `file-count-above-maximum`\n- `total-bytes-above-maximum`\n- `concurrent-uploads-above-maximum`\n\nWhen a typed helper request or live SFTP session supplies `session_info` with\n`principal`, `principal_routes` can place accepted inbound files under a\nprincipal-specific subdirectory while preserving the visible `/inbound/...`\npath for that principal.\n\nVirtual filesystem / exchange example:\n\n```qore\nSftpServerDataProvider::VirtualSftpExchange exchange(\n    \u003cSftpServerDataProvider::VirtualSftpExchangeOptions\u003e{\n        \"archive_dir\": \"/archive\",\n        \"claim_dir\": \"/claimed\",\n        \"inbound_policy\": \u003cSftpServerDataProvider::VirtualSftpInboundPolicy\u003e{\n            \"accept_filename_glob\": \"*.dat\",\n            \"max_size\": 1048576,\n            \"allow_overwrite\": False,\n        },\n    }\n);\n\nexchange.addOutboundData(\"hello.txt\", binary(\"hello\\n\"), {\"kind\": \"example\"});\n\nhash\u003cSftpServerDataProvider::VirtualSftpPathRequest\u003e retrieve_req\n    = \u003cSftpServerDataProvider::VirtualSftpPathRequest\u003e{\"path\": \"/hello.txt\"};\nbinary data = exchange.retrieve(retrieve_req).data;\n\nhash\u003cSftpServerDataProvider::VirtualSftpCompletionRequest\u003e archive_req\n    = \u003cSftpServerDataProvider::VirtualSftpCompletionRequest\u003e{\n        \"path\": \"/hello.txt\",\n        \"completion_mode\": \"archive\",\n    };\nexchange.completeRetrieve(archive_req);\n```\n\nRecord-store example:\n\n```qore\nSftpServerDataProvider::VirtualSftpExchangeRecordStore store(\n    \u003cSftpServerDataProvider::VirtualSftpExchangeOptions\u003e{\n        \"archive_dir\": \"/archive\",\n        \"claim_dir\": \"/claimed\",\n    }\n);\n\nstore.addOutboundRecord(\u003cSftpServerDataProvider::VirtualSftpRecordSubmissionRequest\u003e{\n    \"name\": \"out-1.dat\",\n    \"data\": binary(\"payload\\n\"),\n    \"metadata\": {\"kind\": \"report\"},\n});\n\n    hash\u003cSftpServerDataProvider::VirtualSftpRecordInfo\u003e claimed\n        = store.claimOutboundRecord(\u003cSftpServerDataProvider::VirtualSftpRecordCompletionRequest\u003e{\n            \"name\": \"out-1.dat\",\n            \"final_name\": \"claimed-out-1.dat\",\n            \"completion_mode\": \"claim\",\n        });\n```\n\nTyped backend verb example:\n\n```qore\nSftpServerDataProvider::VirtualSftpExchange exchange();\nexchange.addOutboundData(\"report.txt\", binary(\"payload\\n\"));\n\nhash\u003cQore::Ssh::SftpPathRequest\u003e list_req\n    = \u003cQore::Ssh::SftpPathRequest\u003e{\"path\": \"/outbound\"};\nhash\u003cQore::Ssh::SftpListResult\u003e listing = exchange.list(list_req);\n\nhash\u003cQore::Ssh::SftpOpenRequest\u003e read_req\n    = \u003cQore::Ssh::SftpOpenRequest\u003e{\"path\": \"/outbound/report.txt\"};\n    hash\u003cQore::Ssh::SftpOpenResult\u003e open_result = exchange.openRead(read_req);\n```\n\nGENERIC DATAPROVIDER MODULE\n---------------------------\n`SftpServerDataProvider` is now also a real generic `DataProvider` module,\nnot just a helper module.\n\nCurrent provider surface:\n- factory: `sftpserver`\n- app: `SftpServerExchange`\n- root provider: `SftpServerDataProvider::SftpServerDataProvider`\n- child API providers:\n  - `submit-inbound`\n  - `list-outbound`\n  - `retrieve-outbound`\n  - `claim-outbound`\n  - `archive-outbound`\n  - `delete-outbound`\n  - `cleanup-expired`\n- child event providers:\n  - `inbound-file-accepted`\n    observable entry point for inbound submission events\n    supporting:\n    `inbound-file-accepted` and `inbound-file-rejected`\n  - `outbound-file-claimed`\n  - `outbound-file-archived`\n  - `outbound-file-deleted`\n\nThe current provider layer is intentionally backed by the generic virtual\nexchange helpers:\n- `VirtualSftpExchange`\n- `VirtualSftpExchangeRecordStore`\n\nIt supports:\n- typed request and response data types for the generic exchange operations\n- a typed inbound submission event action for config-driven observable flows\n- typed outbound completion event actions for claim/archive/delete flows\n- a typed retention cleanup action for deterministic inbound/archive/claim cleanup,\n  optionally scoped to a single authenticated principal\n- constructor and action options for inbound acceptance policy\n- direct factory usage through `DataProvider::getFactory(\"sftpserver\")`\n- action-catalog registration for the same generic exchange actions\n- constructor-time backend injection with an existing\n  `VirtualSftpExchangeRecordStore` or `VirtualSftpExchange`\n- direct binding to `SshServerConnections::SftpServerConnection` through the\n  `sftpserver` connection scheme\n\nMinimal example:\n\n```qore\nSftpServerDataProvider::VirtualSftpExchangeRecordStore store(\n    \u003cSftpServerDataProvider::VirtualSftpExchangeOptions\u003e{\n        \"archive_dir\": \"/archive\",\n        \"claim_dir\": \"/claim\",\n        \"inbound_policy\": \u003cSftpServerDataProvider::VirtualSftpInboundPolicy\u003e{\n            \"accept_filename_glob\": \"*.dat\",\n            \"max_size\": 1048576,\n        },\n    }\n);\nstore.addOutboundTextRecord(\"hello.txt\", \"hello\\n\");\n\nAbstractDataProvider provider = DataProvider::getFactory(\"sftpserver\").create({\n    \"backend\": store,\n    \"inbound_policy\": \u003cSftpServerDataProvider::VirtualSftpInboundPolicy\u003e{\n        \"accept_filename_glob\": \"*.dat\",\n        \"max_size\": 1048576,\n    },\n});\n\nhash\u003cauto\u003e list_result = provider.getChildProviderEx(\"list-outbound\").doRequest({});\nhash\u003cauto\u003e retrieve_result = provider.getChildProviderEx(\"retrieve-outbound\").doRequest({\n    \"name\": \"hello.txt\",\n});\nhash\u003cauto\u003e cleanup_result = provider.getChildProviderEx(\"cleanup-expired\").doRequest({\n    \"queue\": SftpServerDataProvider::RetentionQueueInbound,\n    \"now_s\": 1700000000,\n    \"principal\": \"principal:alice\",\n});\n```\n\n`cleanup-expired` can be scoped to a single authenticated principal. The\ncleanup result echoes that principal, and retained inbound files also carry\n`retention_principal` metadata when they were accepted through a principal-aware\nsession or helper request.\n\nEvent example:\n\n```qore\nclass MyObserver inherits DataProvider::Observer {\n    update(string event_id, hash\u003cauto\u003e data_) {\n        printf(\"EVENT %s: %y\\n\", event_id, data_);\n    }\n}\n\nAbstractDataProvider event_provider = provider.getChildProviderEx(\n    SftpServerDataProvider::EventInboundFileAccepted\n);\nMyObserver observer();\ncast\u003cDataProvider::Observable\u003e(event_provider).registerObserver(\n    observer, SftpServerDataProvider::EventInboundFileAccepted);\ncast\u003cDataProvider::Observable\u003e(event_provider).registerObserver(\n    observer, SftpServerDataProvider::EventInboundFileRejected);\ncast\u003cDataProvider::DelayedObservable\u003e(event_provider).observersReady();\n\n// inbound-file-accepted and inbound-file-rejected are the typed event ids.\n// binary payloads are omitted by default; set\n// inbound_event_payload_mode=SftpServerDataProvider::InboundEventPayloadInline\n// when observers need them.\n// live SFTP uploads also include session_id, server_name, listener_name,\n// and principal in the typed event payload.\n```\n\nOutbound completion event providers use the same observer model and emit typed\nclaim/archive/delete events with the operation, relevant completion path,\ntransfer info, optional checksum metadata when the completed record carries it,\nand live session context when the completion came from an authenticated SFTP\nsession.\n\nPolicy-driven server example:\n\n```qore\nSftpServerDataProvider::VirtualSftpExchange exchange(\n    \u003cSftpServerDataProvider::VirtualSftpExchangeOptions\u003e{\n        \"inbound_policy\": \u003cSftpServerDataProvider::VirtualSftpInboundPolicy\u003e{\n            \"accept_filename_glob\": \"*.dat\",\n            \"max_size\": 1048576,\n            \"allow_overwrite\": False,\n        },\n        \"inbound_event_payload_mode\": SftpServerDataProvider::InboundEventPayloadInline,\n    }\n);\n\nSshServerConnections::SftpServerConnection conn({\n    \"name\": \"policy-example\",\n    \"url\": \"sftpserver://127.0.0.1:0\",\n    \"opts\": {\n        \"host_keys\": (\"/path/to/ssh_host_ed25519_key\",),\n        \"services\": \u003cSshServerConnections::SshServerConnectionServices\u003e{\n            \"enabled_auth_methods\": (\"none\",),\n            \"allow_command_service\": False,\n            \"allow_sftp_service\": True,\n        },\n        \"auth\": \u003cSshServerAuthProvider::VirtualSshAuthProviderOptions\u003e{\n            \"methods\": (\"none\",),\n            \"none\": \u003cSshServerAuthProvider::VirtualSshStaticAuthMethodOptions\u003e{\n                \"success_decision\": \u003cSshServerAuthProvider::VirtualSshAuthDecisionTemplate\u003e{\n                    \"principal\": \"principal:nobody\",\n                    \"command_policy\": {\"mode\": \"subsystem-only\", \"allowed_subsystems\": (\"sftp\",)},\n                    \"sftp_policy\": {\"mode\": \"enabled\"},\n                },\n            },\n        },\n        \"backend\": exchange,\n    },\n});\n\nAbstractDataProvider provider = conn.getDataProvider();\nAbstractDataProvider event_provider = provider.getChildProviderEx(\n    SftpServerDataProvider::EventInboundFileAccepted\n);\n```\n\nConnection-driven example:\n\n```qore\nSftpServerDataProvider::VirtualSftpExchangeRecordStore store(\n    \u003cSftpServerDataProvider::VirtualSftpExchangeOptions\u003e{\n        \"archive_dir\": \"/archive\",\n        \"claim_dir\": \"/claim\",\n    }\n);\n\nSshServerConnections::SftpServerConnection conn({\n    \"name\": \"local-sftp-exchange\",\n    \"url\": \"sftpserver://127.0.0.1:0\",\n    \"opts\": {\n        \"host_keys\": (\"/path/to/ssh_host_ed25519_key\",),\n        \"backend\": store,\n    },\n});\n\nAbstractDataProvider provider = conn.getDataProvider();\nhash\u003cauto\u003e list_result = provider.getChildProviderEx(\"list-outbound\").doRequest({});\n```\n\nTyped streamed backend example:\n\n```qore\nclass TypedStreamBackend {\n    hash\u003cQore::Ssh::SftpOpenResult\u003e openRead(hash\u003cQore::Ssh::SftpOpenRequest\u003e req) {\n        return {\n            \"path\": req.path,\n            \"type\": \"file\",\n            \"size\": 8,\n            \"backend_handle\": {\"kind\": \"stream\", \"path\": req.path},\n        };\n    }\n\n    hash\u003cQore::Ssh::SftpReadResult\u003e read(hash\u003cQore::Ssh::SftpReadRequest\u003e req) {\n        return {\n            \"data\": binary(\"payload\\n\"),\n            \"eof\": True,\n        };\n    }\n\n    hash\u003cQore::Ssh::SftpOperationResult\u003e closeRead(hash\u003cQore::Ssh::SftpCloseRequest\u003e req) {\n        return {\n            \"success\": True,\n            \"path\": req.path,\n        };\n    }\n}\n```\n\nEXAMPLES\n--------\nA runnable example is available in:\n    examples/VirtualSftpFilesystem.qtest\n\nAdditional record-store example:\n    examples/VirtualSftpExchangeRecordStore.qtest\n\nLow-level real server example:\n    examples/VirtualSftpServer.qr\n\nReal server policy/event example:\n    examples/VirtualSftpInboundPolicyServer.qr\n\nLICENSE\n-------\nThis module will be released under a choice of two licenses:\n- LGPL 2.1\n- MIT\n\nBUILDING\n--------\nConfigure the local build with the same install prefix as the active `qore`\nbinary, then use the local module build for tests and examples.\n\nTypical local preflight:\n\n```bash\ntest/preflight.sh\n```\n\nThis rebuilds the module, runs the core qtest sweep, runs the live examples,\nand rebuilds the HTML docs.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fqoretechnologies%2Fmodule-ssh","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fqoretechnologies%2Fmodule-ssh","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fqoretechnologies%2Fmodule-ssh/lists"}