moonnut

    A bounded, read-only NUT upsd client for MoonBit native applications

    nut
    ups
    upsd
    monitoring
    Download zip
    Author
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    3 hours ago
    Downloads
    3

    #moonnut

    A small, independently implemented NUT upsd client for MoonBit native. Personally maintained, Apache-2.0 licensed, and deliberately read-only.

    #Scope and safety

    • LIST UPS, LIST VAR and GET VAR, with typed device/variable results.
    • Verified STARTTLS by default, using system roots or an explicit PEM CA file.
    • Optional USERNAME / PASSWORD authentication only after verified TLS.
    • One fresh connection per query; no shared stream, pooling, automatic retries or downgrade.
    • Whole-operation timeout, bounded line length, total response bytes and list rows.
    • NUT-style quoting and escaping, strict reply identity and list boundary checks.

    There is no FSD, SET VAR, INSTCMD, LOGIN, PRIMARY/MASTER, shutdown, or arbitrary-command API. This is not a power-control agent or a complete RFC 9271 implementation. Authentication itself creates temporary server-side session state. Only use it against servers you are authorized to query.

    Verification boundary: tests use synthetic loopback NUT/TLS servers. A common LIST UPS fixture is also compared with PyNUT in a separate process. No physical UPS or real upsd deployment has been verified; no hardware-compatibility claim is made.

    #Quick start

    Requires MoonBit native and moonbitlang/async 0.22.4. Verified locally with moon 0.1.20261104 / moonc v0.10.20+65efd5b93 on macOS arm64. Networking and TLS use the official async library, including its native platform/OpenSSL requirements. The native suite and loopback TLS checks also pass on Ubuntu GitHub Actions.

    # Offline; no device access moon run examples/parse --target native

    Install with moon add pangbit/moonnut or use a local workspace dependency. Import pangbit/moonnut in moon.pkg; a package with async fn main must also import moonbitlang/async.

    ///|
    test "decode a synthetic NUT response" {
    assert_eq(@moonnut.parse_line("VAR demo ups.status \"OL CHRG\""), [
    "VAR", "demo", "ups.status", "OL CHRG",
    ])
    }

    In a native async application, create Client::new("ups.example.invalid"), then call list_ups(), list_vars("demo") or get_var("demo", "ups.status"). Values remain strings; a status like OL CHRG must not be treated as a single enum value. Each call opens, authenticates and closes its own connection, including on failure.

    # Only with authorization for the explicitly named target moon run examples/query --target native -- ups.example.invalid 3493 moon run examples/query --target native -- ups.example.invalid 3493 --ca /path/to/ca.pem

    examples/query reads optional MOONNUT_USERNAME and MOONNUT_PASSWORD from the process environment. Do not put real credentials in commands, source, examples, issues or Git. The example does not print credentials or raw errors. Applications should use a credential provider appropriate to their deployment.

    #API choices

    Client::new options:

    OptionDefaultMeaning
    port3493TCP server port
    securityTlsSystem roots; TlsCaFile(path) uses a custom CA
    username, passwordabsentPaired credentials, TLS only
    timeout_ms5000DNS through the complete response
    max_line_bytes65536Bytes before a line's LF delimiter
    max_response_bytes4194304Total received protocol bytes, including authentication replies
    max_rows4096Rows in a list response

    Plaintext must be explicitly selected and cannot be combined with credentials. It provides neither confidentiality nor server authentication and is intended only for a controlled test or separately secured channel. There is no "trust all certificates" setting. A refused STARTTLS never falls back to plaintext. TLS minimum versions and cipher policy are inherited from the underlying TLS library; moonnut does not currently enforce a separate TLS 1.2/1.3 minimum.

    NutError distinguishes invalid arguments, malformed replies, canonical server error codes, resource limits and premature EOF. Transport, TLS and timeout errors from moonbitlang/async are propagated. A failed call closes its connection; applications decide whether and when to retry. Client is opaque and has no credential-bearing debug representation. Secret memory is not guaranteed to be zeroized.

    #Protocol compatibility and references

    RFC 9271 describes mandatory authentication; historical NUT installations also allow anonymous read-only queries. moonnut permits anonymous reads but respects ERR ACCESS-DENIED; it does not claim the RFC's entire security/profile compliance. It does not depend on newer command aliases such as PROTVER or PRIMARY.

    NUT uses parseconf-style tokens, not JSON or shell syntax: backslash escapes the next character, double quotes group tokens, single quotes do not, unquoted = is a token and unquoted # starts a comment. The encoder also escapes # for older NUT parsers. LF and CRLF replies are accepted; physical newline continuation is not supported. UTF-8 descriptions are accepted as an extension to the documented ASCII protocol. Invalid UTF-8 and control characters are rejected.

    This is original MoonBit code, not a PyNUT translation. PyNUT is GPL-3.0-or-later; no PyNUT source is vendored or linked. The optional comparison runs its public API in a separate process against the same synthetic fixture. That comparison covers LIST UPS names/descriptions only, not all parser edge cases or real-daemon interoperability.

    #Development and verification

    moon update moon check --target native moon test --target native moon build --target native python3 scripts/tls_smoke.py moon info --target native moon fmt --check # Optional: external official PyNUT v2.8.3 module, kept outside the repository python3 scripts/reference_smoke.py /external/path/PyNUT.py # Packaging validation only, not a release moon publish --dry-run

    MoonBit tests cover parsing, injection rejection, fragmented/coalesced replies, error handling, identifier matching, empty lists, EOF, TLS refusal, timeout and resource bounds. The TLS script creates disposable loopback certificates and checks trusted CA, encrypted authentication, unknown CA and wrong hostname. No test needs a real UPS, account or persistent credential. See CONTRIBUTING.md, SECURITY.md and LICENSE.

    NutError

    pub suberror NutError {
    InvalidArgument(String)
    Protocol(String)
    Server(String)
    LimitExceeded(String)
    UnexpectedEof
    } derive(
    Debug
    )

    Errors never include credentials or the raw response line.

    NutError::to_repr

    Client

    pub struct Client {
    // private fields
    }

    Each operation owns a fresh connection, so failures and concurrent calls cannot desynchronize a shared protocol stream.

    Client::get_var

    async fn Client::get_var(self : Client, ups : String, name : String) -> String

    Client::list_ups

    async fn Client::list_ups(self : Client) -> Array[Ups]

    No management or shutdown command is exposed.

    Client::list_vars

    async fn Client::list_vars(self : Client, ups : String) -> Array[Variable]

    Client::new

    fn Client::new(host : String, port? : Int, security? : Security, username? : String, password? : String, timeout_ms? : Int, max_line_bytes? : Int, max_rows? : Int, max_response_bytes? : Int) -> Client raise NutError

    The timeout covers DNS, connection, TLS, authentication and the whole query.

    Security

    pub(all) enum Security {
    Tls
    TlsCaFile(String)
    Plaintext
    }

    TLS is mandatory by default. Plaintext is only for explicitly trusted local networks or an external authenticated tunnel; credentials are never allowed.

    Ups

    pub(all) struct Ups {
    name : String
    description : String
    } derive(Eq,
    Debug
    )

    Ups::equal

    fn Ups::equal(Ups, Ups) -> Bool

    Ups::not_equal

    fn Ups::not_equal(x : Ups, y : Ups) -> Bool

    Ups::to_repr

    Variable

    pub(all) struct Variable {
    name : String
    value : String
    } derive(Eq,
    Debug
    )

    Values remain strings as provided by the server.

    Variable::equal

    fn Variable::equal(Variable, Variable) -> Bool

    Variable::not_equal

    fn Variable::not_equal(x : Variable, y : Variable) -> Bool

    Variable::to_repr

    parse_line

    fn parse_line(line : String) -> Array[String] raise NutError

    Decode a NUT parseconf-style line without its line ending. Supports quoted words, escapes, comments and equals tokens. Physical newline continuation is rejected for network framing.

    quote_argument

    fn quote_argument(value : String) -> String raise NutError

    Quote an argument. Reject controls to prevent command injection.