Skip to content

0xe25f/QwkSync.HTTP

Repository files navigation

QWKSync.HTTP

HTTP/HTTPS transport extension for QWKSync.NET.

Overview

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.

Installation

dotnet add package QwkSync.Http

Quick Start

The 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);
}

Settings Reference

All settings are optional and have sensible defaults. Keys are case-insensitive.

Authentication

Setting Type Default Description
http.username string (none) Username for HTTP Basic authentication.
http.password string (none) Password for HTTP Basic authentication. Never logged.

Download

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).

Upload

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.

TLS / Certificate Validation

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).

Connection

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.

Proxy

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.

Upload Response

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.


Security Notes

TLS Certificate Validation

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 acceptAny in production. A man-in-the-middle attacker can intercept all traffic, including credentials, when certificate validation is disabled.

Certificate Thumbprint Pinning

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.


Proxy Configuration

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.


Limitations

DeleteAsync and MoveAsync are not supported

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 a HEAD request to detect the available QWK packet and its filename.
  • DownloadAsync — issues a GET request and streams the response to a local file.
  • UploadAsync — issues a POST request with the REP file as the body.

Single-packet model

The HTTP endpoint returns at most one QWK packet per HEAD request. ListAsync will therefore return either zero or one RemoteItem.


Licence

MIT Licence. See LICENSE for details.

About

Lightweight HTTP/HTTPS transport extension for QWKSync.NET. Provides file transfer capabilities over HTTP and HTTPS protocols.

Topics

Resources

License

Stars

Watchers

Forks

Contributors

Languages