HTTP/HTTPS transport extension for QWKSync.NET.
QWKSync.HTTP provides HTTP and HTTPS file transfer capabilities for QWKSync.NET, enabling QWK packet downloading and REP packet uploading over standard HTTP/HTTPS endpoints. It is designed primarily for BBS systems running BinktermPHP or compatible HTTP-based QWK APIs.
dotnet add package QwkSync.HttpThe following example demonstrates a typical scenario, connecting to a BBS over HTTPS and downloading a QWK mail packet:
using QwkSync;
using QwkSync.Http;
QwkSyncProfile profile = new()
{
Endpoint = new Uri("https://bbs.example.com"),
TransportId = HttpTransportFactory.HttpTransportId, // "http"
Settings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["http.username"] = "myuser",
["http.password"] = "s3cr3t",
["http.download.path"] = "/qwk/download?format=qwk&limit=2500",
["http.upload.path"] = "/qwk/upload",
["http.upload.fieldName"] = "file",
["http.upload.contentType"] = "multipart",
["http.upload.parseResponse"] = "true",
["http.tls.certValidation"] = "strict",
["http.connectTimeoutMs"] = "15000",
["http.userAgent"] = "QWKSync.NET",
["http.followRedirects"] = "true",
},
};
TransferPolicy policy = new()
{
Timeout = TimeSpan.FromMinutes(2),
MaxRetries = 0,
};
HttpTransportFactory factory = new();
await using ITransport transport = factory.Create(profile, policy);
// List the available QWK packet
IReadOnlyList<RemoteItem> items = await transport.ListAsync(null, "*.qwk", ct);
if (items.Count > 0)
{
RemoteItem packet = items[0];
Console.WriteLine($"Downloading: {packet.Name}");
// Download QWK packet
string localPath = Path.Combine(inboxDirectory, packet.Name);
await transport.DownloadAsync(packet, localPath, progress, ct);
// Upload REP packet
await transport.UploadAsync(localRepPath, null, "replies.rep", progress, ct);
}All settings are optional and have sensible defaults. Keys are case-insensitive.
| Setting | Type | Default | Description |
|---|---|---|---|
http.username |
string |
(none) | Username for HTTP Basic authentication. |
http.password |
string |
(none) | Password for HTTP Basic authentication. Never logged. |
| Setting | Type | Default | Description |
|---|---|---|---|
http.download.path |
string |
/qwk/download?format=qwk&limit=2500 |
Server-relative path and query string used for QWK packet download (HEAD to list, GET to download). |
| Setting | Type | Default | Description |
|---|---|---|---|
http.upload.path |
string |
/qwk/upload |
Server-relative path used for REP packet upload (POST). |
http.upload.fieldName |
string |
file |
Form field name used when uploading with multipart/form-data. |
http.upload.contentType |
multipart | binary |
multipart |
How the REP file is sent. multipart uses multipart/form-data; binary sends the raw file as application/octet-stream. |
http.upload.parseResponse |
true | false |
true |
Whether to parse the server's JSON upload response and throw on failure. Set to false for servers that do not return a JSON body. |
| Setting | Type | Default | Description |
|---|---|---|---|
http.tls.certValidation |
strict | acceptAny |
strict |
Certificate validation mode (see Security Notes). |
http.tls.certThumbprints |
string |
(none) | Comma-separated list of accepted SHA-256 certificate thumbprints for pinning. Each value must be a 64-character hex string (stored normalised to uppercase). |
| Setting | Type | Default | Description |
|---|---|---|---|
http.connectTimeoutMs |
int |
15000 |
Connect (and per-request) timeout in milliseconds. Minimum 1; clamped to 300000 (5 minutes). |
http.userAgent |
string |
QWKSync.NET |
User-Agent header value sent with every request. |
http.followRedirects |
true | false |
true |
Whether HTTP redirects (3xx) are followed automatically. |
| Setting | Type | Default | Description |
|---|---|---|---|
http.proxy.host |
string |
(none) | Proxy server hostname or IP address. Required to enable proxy support. |
http.proxy.port |
int |
(none) | Proxy server port number (1–65535). |
http.proxy.username |
string |
(none) | Proxy authentication username. |
http.proxy.password |
string |
(none) | Proxy authentication password. Never logged. |
When http.upload.parseResponse=true (the default), the transport expects the server to return a
JSON body on upload, structured as follows:
{
"success": true,
"imported": 12,
"skipped": 0,
"errors": []
}If success is false, or if the response body cannot be deserialised, an IOException is
thrown with any server-provided error messages joined together.
Set http.upload.parseResponse=false if connecting to a server that returns a non-JSON body or
no body at all — in that case the transport treats any 2xx HTTP status as a successful upload.
By default (http.tls.certValidation=strict), the transport validates the server's TLS
certificate against the system trust store and rejects any untrusted, expired, or hostname-mismatched
certificates.
acceptAny mode — use with caution
Setting http.tls.certValidation=acceptAny disables all certificate checks and accepts any
certificate, including self-signed ones. This is intended only for local development,
air-gapped networks, or testing environments where you control both the client and the server.
Warning: Never use
acceptAnyin production. A man-in-the-middle attacker can intercept all traffic, including credentials, when certificate validation is disabled.
For enhanced security, you can pin the server's certificate to one or more known SHA-256 thumbprints. When thumbprints are configured, the transport rejects any certificate whose SHA-256 fingerprint is not in the list, even if it would otherwise be trusted by the system store.
Settings = new Dictionary<string, string>
{
["http.tls.certThumbprints"] = "A1B2C3D4...64HEXCHARS,E5F6A7B8...64HEXCHARS",
}Thumbprints must be exactly 64 hexadecimal characters (SHA-256 fingerprint without separators). Values are normalised to uppercase before comparison.
Thumbprint pinning and certValidation=strict are independent — you can combine pinning with
strict validation for maximum security, or use pinning with acceptAny when the certificate is
self-signed but the thumbprint is known and trusted.
HTTP connections can be routed through a proxy server. Any proxy accessible over HTTP/HTTPS is
supported (the underlying WebProxy class is used).
Settings = new Dictionary<string, string>
{
["http.proxy.host"] = "proxy.corp.example.com",
["http.proxy.port"] = "8080",
["http.proxy.username"] = "proxyuser", // optional
["http.proxy.password"] = "proxypass", // optional
}Both http.proxy.host and http.proxy.port are needed for the proxy to be activated. Specifying
only one has no effect.
The HTTP transport does not implement remote file deletion or renaming. Calling either method
throws NotSupportedException immediately:
NotSupportedException: HTTP transport does not support remote file deletion.
NotSupportedException: HTTP transport does not support remote file moving.
This is by design. Most HTTP-based QWK endpoints do not expose file-management operations. The transport is intentionally limited to:
ListAsync— issues aHEADrequest to detect the available QWK packet and its filename.DownloadAsync— issues aGETrequest and streams the response to a local file.UploadAsync— issues aPOSTrequest with the REP file as the body.
The HTTP endpoint returns at most one QWK packet per HEAD request. ListAsync will therefore
return either zero or one RemoteItem.
MIT Licence. See LICENSE for details.