Documentation
¶
Overview ¶
Package http provides the scriptling.runtime.http library: route, static file and WebSocket registration for the scriptling server, plus response helpers. It links gorilla/websocket for the server-side WebSocket wrapper.
Registrations are recorded in State, which shares runtime.RuntimeState's lock with the server lifecycle in the core runtime package: start_server() snapshots the routes (RuntimeState.ServerCollect) under that lock, so a route is either in the snapshot or warned about as registered too late.
Index ¶
- Constants
- Variables
- func CreateRequestInstance(method, path, body string, headers, query, pathParams map[string]string, ...) *object.Instance
- func CreateWebSocketClientInstance(conn *WebSocketServerConn) *object.Instance
- func Register(registrar object.LibraryRegistrar, allowedPaths []string)
- type RouteInfo
- type WebSocketRouteInfo
- type WebSocketServerConn
- func (c *WebSocketServerConn) Close() error
- func (c *WebSocketServerConn) ClosedChan() <-chan struct{}
- func (c *WebSocketServerConn) ID() string
- func (c *WebSocketServerConn) IsConnected() bool
- func (c *WebSocketServerConn) ReadWithTimeout(timeout time.Duration) (int, []byte, error)
- func (c *WebSocketServerConn) RemoteAddr() string
- func (c *WebSocketServerConn) WriteMessage(msgType int, data []byte) error
Constants ¶
const LibraryName = "scriptling.runtime.http"
LibraryName is the name scripts import the library by.
const MaxWebSocketMessage = 8 << 20 // 8 MiB
MaxWebSocketMessage bounds a single inbound frame set. Without it a client can pin as much memory as it cares to send.
Variables ¶
var RequestClass = &object.Class{ Name: "Request", Methods: map[string]object.Object{ "path_param": requestFieldGetter("path_params", "path_param", false, `path_param(name, default=None) - Get a path parameter captured from a route wildcard Route patterns like "/api/users/{id}" capture the matching request path segments. Returns the captured value, default, or None. Parameters: name (string): Path parameter name default (any, optional): Value returned when the parameter is absent`), "query_param": requestFieldGetter("query", "query_param", false, `query_param(name, default=None) - Get a query parameter Returns the first value of the query parameter, default, or None. Parameters: name (string): Query parameter name default (any, optional): Value returned when the parameter is absent`), "header": requestFieldGetter("headers", "header", true, `header(name, default=None) - Get a request header Header names are case-insensitive. Returns the header value, default, or None. Parameters: name (string): Header name default (any, optional): Value returned when the header is absent`), "json": &object.Builtin{ Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object { if err := errors.ExactArgs(args, 1); err != nil { return err } instance, ok := args[0].(*object.Instance) if !ok { return errors.NewError("json() called on non-Request object") } body, err := instance.Field("body").AsString() if err != nil { return err } if body == "" { return &object.Null{} } return conversion.MustParseJSON(body) }, HelpText: `json() - Parse request body as JSON Returns the parsed JSON as a dict or list, or None if body is empty.`, }, }, }
RequestClass is the class for Request objects passed to handlers
var State = struct { // HTTP routes, keyed "METHOD path" Routes map[string]*RouteInfo Middleware string NotFoundHandler string // WebSocket routes and connections WebSocketRoutes map[string]*WebSocketRouteInfo WebSocketConnections map[string]*WebSocketServerConn }{ Routes: make(map[string]*RouteInfo), WebSocketRoutes: make(map[string]*WebSocketRouteInfo), WebSocketConnections: make(map[string]*WebSocketServerConn), }
State holds the registrations made through scriptling.runtime.http. It is guarded by runtime.RuntimeState's lock (not a lock of its own) so that start_server() can snapshot it atomically with the server lifecycle; hold runtime.RuntimeState.Lock (or RLock to read) while touching it. runtime.ResetRuntime clears it and closes the open WebSocket connections.
var WebSocketClientClass = &object.Class{ Name: "WebSocketClient", Methods: map[string]object.Object{ "connected": &object.Builtin{ Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object { if err := errors.ExactArgs(args, 1); err != nil { return err } instance, ok := args[0].(*object.Instance) if !ok { return errors.NewError("connected() called on non-WebSocketClient object") } conn := getWSConnFromInstance(instance) if conn == nil { return object.NewBoolean(false) } return object.NewBoolean(conn.IsConnected()) }, HelpText: `connected() - Check if the WebSocket connection is still open Returns True if connected, False otherwise.`, }, "receive": &object.Builtin{ Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object { if err := errors.MinArgs(args, 1); err != nil { return err } instance, ok := args[0].(*object.Instance) if !ok { return errors.NewError("receive() called on non-WebSocketClient object") } timeout := 30.0 if t := kwargs.Get("timeout"); t != nil { if timeoutFloat, e := t.AsFloat(); e == nil { timeout = timeoutFloat } } conn := getWSConnFromInstance(instance) if conn == nil { return &object.Null{} } msgType, data, err := conn.ReadWithTimeout(time.Duration(timeout * float64(time.Second))) if err != nil || data == nil { return &object.Null{} } if msgType == websocket.TextMessage { return object.NewString(string(data)) } elements := make([]object.Object, len(data)) for i, b := range data { elements[i] = object.NewInteger(int64(b)) } return &object.List{Elements: elements} }, HelpText: `receive(timeout=30) - Receive a message from the WebSocket Parameters: timeout (number, optional): Timeout in seconds (default: 30) Returns: string for text messages, list of bytes for binary, or None on timeout/disconnect`, }, "send": &object.Builtin{ Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object { if err := errors.MinArgs(args, 2); err != nil { return err } instance, ok := args[0].(*object.Instance) if !ok { return errors.NewError("send() called on non-WebSocketClient object") } msg := args[1] var data []byte if dict, ok := msg.(*object.Dict); ok { jsonData, jsonErr := json.Marshal(conversion.ToGo(dict)) if jsonErr != nil { return errors.NewError("failed to encode JSON: %s", jsonErr.Error()) } data = jsonData } else if str, ok := msg.(*object.String); ok { data = []byte(str.StringValue()) } else { strVal, coerceErr := object.CoerceWireString(msg) if coerceErr != nil { return errors.NewError("message must be string or dict") } data = []byte(strVal) } conn := getWSConnFromInstance(instance) if conn == nil { return errors.NewError("connection closed") } if writeErr := conn.WriteMessage(websocket.TextMessage, data); writeErr != nil { return errors.NewError("send failed: %s", writeErr.Error()) } return &object.Null{} }, HelpText: `send(message) - Send a message to the WebSocket client Parameters: message (string or dict): Message to send. Dicts are automatically JSON encoded.`, }, "send_binary": &object.Builtin{ Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object { if err := errors.MinArgs(args, 2); err != nil { return err } instance, ok := args[0].(*object.Instance) if !ok { return errors.NewError("send_binary() called on non-WebSocketClient object") } list, ok := args[1].(*object.List) if !ok { return errors.NewError("send_binary requires a list of bytes") } data := make([]byte, len(list.Elements)) for i, elem := range list.Elements { b, e := elem.AsInt() if e != nil || b < 0 || b > 255 { return errors.NewError("send_binary requires list of bytes (0-255)") } data[i] = byte(b) } conn := getWSConnFromInstance(instance) if conn == nil { return errors.NewError("connection closed") } if writeErr := conn.WriteMessage(websocket.BinaryMessage, data); writeErr != nil { return errors.NewError("send_binary failed: %s", writeErr.Error()) } return &object.Null{} }, HelpText: `send_binary(data) - Send binary data to the WebSocket client Parameters: data (list): List of byte values (0-255)`, }, "close": &object.Builtin{ Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object { if err := errors.ExactArgs(args, 1); err != nil { return err } instance, ok := args[0].(*object.Instance) if !ok { return errors.NewError("close() called on non-WebSocketClient object") } conn := getWSConnFromInstance(instance) if conn != nil { conn.Close() } return &object.Null{} }, HelpText: `close() - Close the WebSocket connection`, }, }, }
WebSocketClientClass is the class for WebSocket client objects passed to handlers
Functions ¶
func CreateRequestInstance ¶
func CreateRequestInstance(method, path, body string, headers, query, pathParams map[string]string, remoteAddr string) *object.Instance
CreateRequestInstance creates a new Request instance with the given data. pathParams holds values captured from route wildcards ("{id}" and "{path...}") and remoteAddr the client address; both are exposed as request fields (path_params, remote_addr). The context field starts as an empty dict for middleware to populate — see WithRequestContext.
func CreateWebSocketClientInstance ¶
func CreateWebSocketClientInstance(conn *WebSocketServerConn) *object.Instance
CreateWebSocketClientInstance creates a new WebSocketClient instance
func Register ¶
func Register(registrar object.LibraryRegistrar, allowedPaths []string)
Register registers the scriptling.runtime.http library. allowedPaths restricts the directories static() may serve, as it restricts the filesystem libraries: nil leaves them unrestricted, an empty slice denies every one. A directory outside the allowed paths is refused with a PermissionError that a script cannot catch, and the server serves only files that resolve inside the allowed paths.
When scriptling.runtime is also registered with the same registrar the library is reachable as an attribute of it (`rt.http`) too.
Types ¶
type RouteInfo ¶
type RouteInfo struct {
Methods []string
Handler string
Static bool
StaticDir string
// StaticAllowed is the --allowed-paths set in force when the static route
// was registered (absolute); nil means unrestricted. The server serves
// only files that resolve inside it, and hides dotfiles and listings.
StaticAllowed []string
}
RouteInfo stores information about a registered route
type WebSocketRouteInfo ¶
type WebSocketRouteInfo struct {
Handler string // "library.function" to call for each connection
}
WebSocketRouteInfo stores information about a registered WebSocket route
type WebSocketServerConn ¶
type WebSocketServerConn struct {
// contains filtered or unexported fields
}
WebSocketServerConn wraps a server-side WebSocket connection.
Locking: mu guards state, readMu serializes readers, and writeMu serializes writers. Neither I/O lock is needed by Close, so closing the transport can interrupt a blocked read or write while preserving Gorilla's one-reader / one-writer rules.
func NewWebSocketServerConn ¶
func NewWebSocketServerConn(conn *websocket.Conn, id string) *WebSocketServerConn
NewWebSocketServerConn creates a new server WebSocket connection wrapper
func (*WebSocketServerConn) Close ¶
func (c *WebSocketServerConn) Close() error
Close closes the connection
func (*WebSocketServerConn) ClosedChan ¶
func (c *WebSocketServerConn) ClosedChan() <-chan struct{}
ClosedChan returns a channel that closes when the connection closes
func (*WebSocketServerConn) ID ¶
func (c *WebSocketServerConn) ID() string
ID returns the connection ID
func (*WebSocketServerConn) IsConnected ¶
func (c *WebSocketServerConn) IsConnected() bool
IsConnected returns whether the connection is still open
func (*WebSocketServerConn) ReadWithTimeout ¶
ReadWithTimeout reads a message with timeout. Returns messageType, data, error. On timeout, returns 0, nil, nil
func (*WebSocketServerConn) RemoteAddr ¶
func (c *WebSocketServerConn) RemoteAddr() string
RemoteAddr returns the remote address
func (*WebSocketServerConn) WriteMessage ¶
func (c *WebSocketServerConn) WriteMessage(msgType int, data []byte) error
WriteMessage sends a message