Skip to content

Network operations

A plugin has no sockets of its own. Every connection is opened by the app, after the host checked the permission, the address and the limits described here. For the envelope, async mechanism and the common limits table see System operations. The user guide is Network.

Permission Allows
net.https net.fetch to public https:// URLs
net.ws net.ws.connect to public wss:// URLs
net.tcp net.tcp.connect to public hosts (optionally with TLS)
net.udp net.udp.connect to public hosts
net.local the same calls to this computer only (loopback), and only to the ports listed in the manifest ports field

net.local takes ports in the manifest: 1 to 16 entries, each an integer (8080) or a text range ("9000-9010"), values 1 to 65535. A loopback target whose port is not listed is denied “this port”. net.local does not reach the LAN, only the loopback host names below.

Permission and address are decided per call. For an already open socket the permission is checked again on every net.socket.* / net.ws.* call (denied “socket” if it was revoked).

For a public target (everything except the loopback targets below) the host accepts:

Rule Detail
Scheme https:// for net.fetch, wss:// for net.ws.connect. http:// and ws:// are refused (invalid “only https URLs are allowed” / “url must start with wss://”)
URL at most 2048 bytes; only ASCII from 0x21 to 0x7E (no spaces, no control or non-ASCII characters; use percent-encoding and punycode); fragment is dropped
User info a URL with @ in the authority is refused: invalid “the URL has no usable host”
Port 1 to 65535, any port; default 443. An IPv6 address must be in brackets
Host name at most 253 characters, labels up to 63 of letters, digits, -, _; case-insensitive; trailing dots removed; a last label that starts with a digit is refused (blocks 127.1, 0x7f.1 style tricks): invalid “a bad host name”
Refused names localhost, *.localhost, *.local, *.internal, *.home.arpa: invalid “local hosts are not allowed”
IP literals must be public: refused with invalid “the address is not public” for 0.0.0.0/8, 10/8, 100.64/10, 127/8, 169.254/16, 172.16/12, 192.0.0/24, 192.0.2/24, 192.168/16, 198.18/15, 198.51.100/24, 203.0.113/24, multicast and above (224/4 and up); IPv6 ::, ::1, fc00::/7, ff00::/8, fe80::/10, fec0::/10, 2001:db8::/32, 100::/64, 64:ff9b:1::/48; IPv4-mapped (::ffff:a.b.c.d), IPv4-compatible, NAT64 (64:ff9b::/96), 6to4 (2002::/16) and Teredo (2001::/32) addresses are judged by the IPv4 address they contain

For a host name the app resolves it itself and applies the rule to every address of the answer: if any address is not public the call fails with transport code blocked (“the address is not public”). The connection then goes to the first address of that same answer, so the name is not resolved a second time between the check and the connection (no DNS rebinding window). Every redirect hop is checked again from scratch.

With a proxy configured in the app (see below) the name is sent to the proxy and resolved there; the app does not see the address, so the DNS-answer check cannot be applied. IP literals are still checked before the call.

A target is local when the host is localhost, an address in 127.0.0.0/8, or ::1 ([::1] in URLs); a trailing dot is ignored. Local targets:

  • need net.local (denied “net.local”) instead of the public permission, and an allowed port;
  • for net.fetch, may use http:// or https:// (default ports 80 and 443); ws:// and wss:// for net.ws.connect; any other scheme is invalid;
  • are never sent through the proxy and never follow redirects;
  • at the transport level resolve only to loopback addresses (blocked otherwise).

Permission net.https (public) or net.local (loopback). Async supported.

Argument Type Required Default Validation
url text yes rules above; invalid “method or url” if missing/too long, or the parser’s message
method text no "GET" exactly GET, POST, PUT, PATCH, DELETE or HEAD (case sensitive); other text of up to 8 bytes is invalid “method or url”. A value that is not text, or is longer than 8 bytes, is silently replaced by GET
headers map text to text no none up to 32 entries, see below
body bytes, text or null no none text is sent as UTF-8; other types invalid “body”; over 100 MiB limit “the body is too large”
timeout integer, ms no 10000 clamped to 100 to 300000; a non-integer is ignored (10000)

Result:

{"status": 200,
"headers": {"content-type": "text/html", ...}, # lower-case names, repeated names joined with ", "
"header_list": [["content-type", "text/html"], ...], # every header in order, lower-case names
"body": <bytes>}

Set-Cookie and Set-Cookie2 are removed from both headers and header_list. HTTP error statuses (4xx, 5xx) are a normal result, not an error. In Python, net.fetch returns Response (status, headers, header_list, body, text(), json(), ok); net.get and net.post are shortcuts.

r = freshgram.net.fetch("https://example.org/api", method="POST",
headers={"Content-Type": "application/json"}, body=b"{}")
# raw: {"op": "net.fetch", "a": {"url": "https://example.org/api", "method": "POST",
# "headers": {"Content-Type": "application/json"}, "body": <bytes>, "timeout": 10000}}
Rule Detail
Count and size at most 32 headers, value at most 4096 bytes, name 1 to 64 characters
Name letters, digits and the symbols ! # $ % & ' * + - . ^ _ ~, the backtick and the vertical bar
Value text; no control characters except tab (no CR, LF)
Forbidden names Host, Content-Length, Transfer-Encoding, Connection, Upgrade, TE, Trailer, Expect, Cookie, Cookie2, Proxy-Authorization, Proxy-Connection, Keep-Alive, Via, Forwarded, X-Forwarded-For, X-Forwarded-Host, X-Forwarded-Proto, X-Real-IP, anything starting with Proxy- or Sec- (case-insensitive)

A header that breaks a rule is invalid “header ”. The host sends Host, Connection: close, Accept-Encoding: identity itself, adds Content-Length for bodies and for POST/PUT/PATCH, and User-Agent: freshGram-plugin unless you set User-Agent. Responses are never decompressed (the request asks for identity).

Only for public targets. Up to 5 hops, statuses 301, 302, 303, 307, 308 with a Location header (relative locations are resolved). The target must be https:// and pass all the address rules; otherwise the call fails with redirect “a redirect is not allowed”; more than 5: redirect “too many redirects”. 303, and 301/302 after POST, switch to GET with no body; 307/308 keep method and body. The Authorization header is dropped when the host changes. A redirect status without Location is returned as the result. Local requests do not follow redirects (the 3xx answer is the result). One deadline (timeout) covers the whole chain.

Limit Value
Response body 100 MiB (limit “the response is too large”; also checked early from Content-Length and while decoding chunks)
Response header block 64 KiB (response “the headers are too large”)
Request body 100 MiB
timeout whole request including connect, TLS, redirects: timeout “the request timed out”

The body is read by Content-Length, by chunked decoding, or until the server closes the connection. A malformed answer is response. Note that a HEAD reply that carries a Content-Length makes the host wait for body bytes that never come, so it ends with response “the connection closed early”; prefer GET when you need to inspect such a resource.

The host speaks TLS 1.2 or newer, verifies the certificate chain against the system trust store and checks that the certificate matches the host name from the URL. There is no way to disable verification or pin a certificate. The same applies to tls=True TCP sockets and wss://. A failure is tls.

Code Trigger
unavailable “network” the app provides no network service
invalid bad method/URL/headers/body (see tables)
denied net.https / net.local not granted; denied “this port” for a local port not in ports
limit body or response too large; rate limits of the transport
transport codes see below

Permission net.tcp / net.udp, or net.local when host is a loopback host. Async supported (runs on the socket thread).

Argument Type Required Default Validation
host text yes up to 255 bytes, public host rules (IP literal or name); loopback hosts switch to the net.local rules
port integer yes 1 to 65535; for loopback it must also be in the manifest ports, else denied “this port”
tls bool no false TCP only. true on UDP or a non-bool value: invalid “tls”
timeout integer, ms no 10000 clamped to 0 to 30000, then at least 100 (applies to the connection and TLS handshake); a non-integer is ignored (10000)

Result: {"id": <socket id>}. Socket ids are shared by TCP, UDP and WebSocket. A tls=True connection verifies the certificate against host. At most 16 open sockets per kind: limit “too many open sockets”. A UDP socket is bound to a single peer: it sends to that peer and only receives datagrams that come from it (and the relay when proxied).

s = freshgram.net.tcp("example.org", 443, tls=True) # -> Socket
# raw: {"op": "net.tcp.connect", "a": {"host": "example.org", "port": 443, "tls": true, "timeout": 10000}}

Errors: denied (permission, port), invalid (“host or port”, “tls”, policy messages such as “the address is not public”), limit, and the transport codes.

net.socket.send, net.socket.recv, net.socket.close

Section titled “net.socket.send, net.socket.recv, net.socket.close”

Apply to TCP and UDP sockets. A WebSocket id used here is invalid “this socket does not take that call”; an unknown or closed id is not_found “socket” (the same for net.ws.send, net.ws.recv and net.ws.close).

Argument Type Required Default Validation
id integer yes open socket
data bytes or text yes text is sent as UTF-8; not empty; up to 1 MiB (TCP) or 65507 bytes (UDP); else limit “the data is too large or empty”; wrong type invalid “data”
timeout integer, ms no 10000 clamped to 0 to 30000, then at least 100; a non-integer is ignored (10000)

Result: {"sent": <bytes>}. UDP is limited to 200 datagrams per second per socket: limit “too many datagrams”.

Argument Type Required Default Validation
id integer yes
max integer no 65507 clamped to 1 to 1048576; a non-integer is ignored (65507)
timeout integer, ms no 1000 clamped to 0 to 30000; a non-integer is ignored (1000)

Result: {"data": <bytes>, "closed": <bool>}. For TCP it waits until at least one byte is available, the connection closes or the timeout passes; data is empty when nothing arrived. closed is true once the peer closed and nothing is left to read. For UDP it returns one datagram (truncated to max); datagrams from other senders are discarded.

data, closed = s.recv(max_bytes=4096, timeout=1000)
# raw: {"op": "net.socket.recv", "a": {"id": 5, "max": 4096, "timeout": 1000}}

Argument id. Result null. The id becomes invalid. Sockets are also closed when the plugin stops, and the user can close any connection from the app (later calls then fail with closed).

net.ws.connect, net.ws.send, net.ws.recv, net.ws.close

Section titled “net.ws.connect, net.ws.send, net.ws.recv, net.ws.close”

Permission net.ws (public wss://), or net.local for ws:///wss:// to loopback. Async supported.

Argument Type Required Default Validation
url text yes up to 2048 bytes; a missing, non-text or longer URL is invalid “url”; wss:// for public hosts (the URL rules above); invalid “url must start with wss://” otherwise
protocols array of text no none up to 8 names, each 1 to 64 of letters, digits, . _ - +; else invalid “protocols”
headers map no none the same rules and forbidden names as net.fetch; Sec-* are forbidden
timeout integer, ms no 10000 clamped to 100 to 30000 (connect, TLS and handshake together); a non-integer is ignored (10000)

Result: {"id": <socket id>, "protocol": "<chosen subprotocol or empty>"}. The handshake must return 101 with a valid Sec-WebSocket-Accept, Upgrade: websocket and Connection: Upgrade; the host accepts no extensions (so no compression) and no subprotocol that was not offered; handshake response up to 16 KiB. Failures are failed with a message such as “the server refused: HTTP/1.1 403”, “the server chose an extension”. At most 16 open WebSockets.

Argument Type Required Default Validation
id integer yes a WebSocket id
data text or bytes yes text goes as a text frame, bytes as a binary frame; up to 1 MiB else limit “the message is too large”; wrong type invalid “data”
timeout integer, ms no 10000 clamped to 0 to 30000, then at least 100; a non-integer is ignored (10000)

Result: {"sent": <bytes>}. After a close was sent or received: failed “the connection is closed”.

Argument Type Required Default Validation
id integer yes
timeout integer, ms no 1000 clamped to 0 to 30000; a non-integer is ignored (1000)

Result is one of:

kind Other fields Meaning
"text" data (text), closed: false a text message (must be valid UTF-8, otherwise the host closes with 1007 and answers failed)
"binary" data (bytes), closed: false a binary message
"close" code, reason, closed: true the server closed (code 1005 when no code was sent; 1006 when the connection dropped)
"none" closed: false nothing arrived within timeout

The host answers pings itself and echoes the close. A server frame over 1 MiB, a fragmented message that grows over 1 MiB (limit “the message is too large”, close 1009), masked or reserved-bit frames, or a bad frame order are protocol errors (failed “the server broke the protocol”, close 1002).

Argument Type Required Default Validation
id integer yes
code integer no 1000 1000 or 3000 to 4999, else invalid “code or reason”
reason text no "" up to 123 bytes

Result null. The socket id becomes invalid.

ws = freshgram.net.ws("wss://example.org/feed", protocols=["v1"])
msg = ws.recv(timeout=1000)
ws.send("hello")
ws.close()
# raw: {"op": "net.ws.recv", "a": {"id": 3, "timeout": 1000}}

The user can set a network policy in the app. It is applied by the transport to everything a plugin opens:

Setting Effect
Block HTTPS / TCP / UDP calls of that kind fail with blocked (“… is blocked for plugins”); open connections of the kind are cut. net.ws and tls=True sockets count as TCP
Proxy (SOCKS5 or HTTP CONNECT, optional login) public TCP, TLS, WebSocket and net.fetch traffic goes through it, the host name is passed to the proxy; UDP works through SOCKS5 (UDP ASSOCIATE) and fails with unavailable “UDP is not supported through an HTTP proxy” for an HTTP proxy. Loopback traffic ignores the proxy. Proxy errors are connect with a “proxy: …” message
Connection list / log every request and socket is shown to the user and can be closed

These are produced by the app while connecting and transferring; in Python they all become Failed (read e.code), except invalid, limit and unavailable, which have their own classes.

Code Meaning
blocked user policy blocks that kind of traffic, or an address is not public (or not loopback for local targets)
dns the host name was not found
connect connection or proxy failure, refused, unreachable, connect timeout
tls TLS handshake failed, certificate rejected, handshake timed out
timeout the request or a send did not finish in time
closed the connection was closed (by the peer, the user, or on plugin stop)
write the send failed
response malformed HTTP answer, oversized headers, early close
redirect redirect refused or too many redirects
limit too many datagrams, or an HTTP body over the limit
invalid a bad address or URL found while connecting
unavailable e.g. UDP through an HTTP proxy

Both net.socket.recv and net.ws.recv use the timeout only to wait; a timeout is not an error (empty data / kind: "none").