gateway

package
v0.112.6-nightly Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 24, 2026 License: AGPL-3.0 Imports: 73 Imported by: 0

Documentation

Overview

Package gateway provides the main API Gateway for the Orama Network. It orchestrates traffic between clients and various backend services including distributed caching (Olric), decentralized storage (IPFS), and serverless WebAssembly (WASM) execution. The gateway implements robust security through wallet-based cryptographic authentication and JWT lifecycle management.

Index

Constants

View Source
const (
	// HeaderInternalAuthNamespace contains the validated namespace name
	HeaderInternalAuthNamespace = "X-Internal-Auth-Namespace"
	// HeaderInternalAuthValidated indicates the request was pre-authenticated by main gateway
	HeaderInternalAuthValidated = "X-Internal-Auth-Validated"
)

Internal auth headers for trusted inter-gateway communication. When the main gateway proxies to a namespace gateway, it validates auth first and passes the validated namespace via these headers. The namespace gateway trusts these headers when they come from internal IPs (WireGuard 10.0.0.x).

View Source
const (
	CtxKeyNamespaceOverride = ctxkeys.NamespaceOverride
)

Context keys for request-scoped values

View Source
const (
	// DefaultMaxConnections is the maximum number of concurrent connections per server.
	DefaultMaxConnections = 10000
)

Variables

View Source
var (
	BuildVersion = "dev"
	BuildCommit  = ""
	BuildTime    = ""
)

Build info (set via -ldflags at build time; defaults for dev)

Functions

func GetWireGuardIP

func GetWireGuardIP() (string, error)

GetWireGuardIP detects the local WireGuard IP address using the wg0 network interface, the 'ip' command, or the WireGuard config file. It does not require a PeerDiscovery instance and can be called from anywhere in the gateway package.

func IsResponseFailure

func IsResponseFailure(statusCode int) bool

IsResponseFailure checks if an HTTP response status indicates a backend failure that should count toward the circuit breaker threshold.

func LimitedListener

func LimitedListener(l net.Listener, maxConns int) net.Listener

LimitedListener wraps a net.Listener with a maximum concurrent connection limit. When the limit is reached, new connections block until an existing one closes.

Types

type ACMERequest

type ACMERequest struct {
	FQDN  string `json:"fqdn"`  // e.g., "_acme-challenge.example.com."
	Value string `json:"value"` // The challenge token
}

ACMERequest represents the request body for ACME DNS-01 challenges from the lego httpreq provider

type CircuitBreaker

type CircuitBreaker struct {
	// contains filtered or unexported fields
}

CircuitBreaker implements the circuit breaker pattern per target.

func NewCircuitBreaker

func NewCircuitBreaker() *CircuitBreaker

NewCircuitBreaker creates a circuit breaker with default settings.

func (*CircuitBreaker) Allow

func (cb *CircuitBreaker) Allow() bool

Allow checks whether a request should be allowed through. Returns false if the circuit is open (fast-fail).

func (*CircuitBreaker) RecordFailure

func (cb *CircuitBreaker) RecordFailure()

RecordFailure records a failed response, potentially opening the circuit.

func (*CircuitBreaker) RecordSuccess

func (cb *CircuitBreaker) RecordSuccess()

RecordSuccess records a successful response, resetting the circuit.

type CircuitBreakerRegistry

type CircuitBreakerRegistry struct {
	// contains filtered or unexported fields
}

CircuitBreakerRegistry manages per-target circuit breakers.

func NewCircuitBreakerRegistry

func NewCircuitBreakerRegistry() *CircuitBreakerRegistry

NewCircuitBreakerRegistry creates a new registry.

func (*CircuitBreakerRegistry) Get

Get returns (or creates) a circuit breaker for the given target key.

type CircuitState

type CircuitState int

CircuitState represents the current state of a circuit breaker

const (
	CircuitClosed   CircuitState = iota // Normal operation
	CircuitOpen                         // Fast-failing
	CircuitHalfOpen                     // Probing with a single request
)

type Config

type Config struct {
	ListenAddr      string
	ClientNamespace string
	BootstrapPeers  []string
	NodePeerID      string // The node's actual peer ID from its identity file

	// Optional DSN for rqlite database/sql driver, e.g. "http://localhost:4001"
	// If empty, defaults to "http://localhost:4001".
	RQLiteDSN string

	// Global RQLite DSN for API key validation (for namespace gateways)
	// If empty, uses RQLiteDSN (for main/global gateways)
	GlobalRQLiteDSN string

	// HTTPS configuration
	EnableHTTPS bool   // Enable HTTPS with ACME (Let's Encrypt)
	DomainName  string // Domain name for HTTPS certificate
	TLSCacheDir string // Directory to cache TLS certificates (default: ~/.orama/tls-cache)

	// Domain routing configuration
	BaseDomain string // Base domain for deployment routing. Set via node config http_gateway.base_domain. Defaults to "dbrs.space"

	// Data directory configuration
	DataDir string // Base directory for node-local data (SQLite databases, deployments). Defaults to ~/.orama

	// Olric cache configuration
	OlricServers []string      // List of Olric server addresses (e.g., ["localhost:3320"]). If empty, defaults to ["localhost:3320"]
	OlricTimeout time.Duration // Timeout for Olric operations (default: 10s)

	// IPFS Cluster configuration
	IPFSClusterAPIURL     string        // IPFS Cluster HTTP API URL (e.g., "http://localhost:9094"). If empty, gateway will discover from node configs
	IPFSAPIURL            string        // IPFS HTTP API URL for content retrieval (e.g., "http://localhost:4501"). If empty, gateway will discover from node configs
	IPFSTimeout           time.Duration // Timeout for IPFS operations (default: 60s)
	IPFSReplicationFactor int           // Replication factor for pins (default: 3)
	IPFSEnableEncryption  bool          // Enable client-side encryption before upload (default: true, discovered from node configs)

	// WireGuard mesh configuration
	ClusterSecret string // Cluster secret for authenticating internal WireGuard peer exchange

	// WebRTC configuration (set when namespace has WebRTC enabled)
	WebRTCEnabled bool   // Whether WebRTC endpoints are active on this gateway
	SFUPort       int    // Local SFU signaling port to proxy WebSocket connections to
	TURNDomain    string // TURN server domain for credential generation
	TURNSecret    string // HMAC-SHA1 shared secret for TURN credential generation
}

Config holds configuration for the gateway server

func (*Config) ValidateConfig added in v0.51.5

func (c *Config) ValidateConfig() []error

ValidateConfig performs comprehensive validation of gateway configuration. It returns aggregated errors, allowing the caller to print all issues at once.

type Dependencies added in v0.90.0

type Dependencies struct {
	// Client is the network client for P2P communication
	Client client.NetworkClient

	// RQLite database dependencies
	SQLDB     *sql.DB
	ORMClient rqlite.Client
	ORMHTTP   *rqlite.HTTPGateway

	// Olric distributed cache client
	OlricClient *olric.Client

	// IPFS storage client
	IPFSClient ipfs.IPFSClient

	// Serverless function engine components
	ServerlessEngine   *serverless.Engine
	ServerlessRegistry *serverless.Registry
	ServerlessInvoker  *serverless.Invoker
	ServerlessWSMgr    *serverless.WSManager
	ServerlessHandlers *serverlesshandlers.ServerlessHandlers

	// PubSub trigger dispatcher (used to wire into PubSubHandlers)
	PubSubDispatcher *triggers.PubSubDispatcher

	// Authentication service
	AuthService *auth.Service
}

Dependencies holds all service clients and components required by the Gateway. This struct encapsulates external dependencies to support dependency injection and testability.

func NewDependencies added in v0.90.0

func NewDependencies(logger *logging.ColoredLogger, cfg *Config) (*Dependencies, error)

NewDependencies creates and initializes all gateway dependencies based on the provided configuration. It establishes connections to RQLite, Olric, IPFS, initializes the serverless engine, and creates the authentication service.

type ExpoPushMessage added in v0.72.0

type ExpoPushMessage struct {
	To       string                 `json:"to"`
	Title    string                 `json:"title"`
	Body     string                 `json:"body"`
	Data     map[string]interface{} `json:"data,omitempty"`
	Sound    string                 `json:"sound,omitempty"`
	Badge    int                    `json:"badge,omitempty"`
	Priority string                 `json:"priority,omitempty"`
	// iOS specific
	MutableContent bool   `json:"mutableContent,omitempty"`
	IosIcon        string `json:"iosIcon,omitempty"`
	// Android specific
	AndroidBigLargeIcon string `json:"androidBigLargeIcon,omitempty"`
	ChannelID           string `json:"channelId,omitempty"`
}

ExpoPushMessage represents a message to send via Expo

type ExpoTicket added in v0.72.0

type ExpoTicket struct {
	ID    string `json:"id"`
	Error string `json:"error,omitempty"`
}

ExpoTicket represents the response from Expo API

type Gateway

type Gateway struct {
	// contains filtered or unexported fields
}

func New

func New(logger *logging.ColoredLogger, cfg *Config) (*Gateway, error)

New creates and initializes a new Gateway instance. It establishes all necessary service connections and dependencies.

func (*Gateway) Close

func (g *Gateway) Close()

Close gracefully shuts down the gateway and all its dependencies. It closes the serverless engine, network client, database connections, Olric cache client, and IPFS client in sequence.

func (*Gateway) GetIPFSClient

func (g *Gateway) GetIPFSClient() ipfs.IPFSClient

GetIPFSClient returns the IPFS client for external use (e.g., by namespace delete handler)

func (*Gateway) GetORMClient

func (g *Gateway) GetORMClient() rqlite.Client

GetORMClient returns the RQLite ORM client for external use (e.g., by ClusterManager)

func (*Gateway) Routes

func (g *Gateway) Routes() http.Handler

Routes returns the http.Handler with all routes and middleware configured

func (*Gateway) SetClusterProvisioner

func (g *Gateway) SetClusterProvisioner(cp authhandlers.ClusterProvisioner)

SetClusterProvisioner sets the cluster provisioner for namespace cluster management. This enables automatic RQLite/Olric/Gateway cluster provisioning when new namespaces are created.

func (*Gateway) SetNamespaceDeleteHandler

func (g *Gateway) SetNamespaceDeleteHandler(h http.Handler)

SetNamespaceDeleteHandler sets the handler for namespace deletion requests.

func (*Gateway) SetNamespaceListHandler

func (g *Gateway) SetNamespaceListHandler(h http.Handler)

SetNamespaceListHandler sets the handler for namespace list requests.

func (*Gateway) SetNodeRecoverer

func (g *Gateway) SetNodeRecoverer(nr authhandlers.NodeRecoverer)

SetNodeRecoverer sets the handler for dead node recovery and revived node cleanup.

func (*Gateway) SetSpawnHandler

func (g *Gateway) SetSpawnHandler(h http.Handler)

SetSpawnHandler sets the handler for internal namespace spawn/stop requests.

func (*Gateway) SetWebRTCManager

func (g *Gateway) SetWebRTCManager(wm authhandlers.WebRTCManager)

SetWebRTCManager sets the WebRTC lifecycle manager for enable/disable operations.

type GatewayInstance

type GatewayInstance struct {
	Namespace    string
	NodeID       string
	HTTPPort     int
	BaseDomain   string
	RQLiteDSN    string   // Connection to namespace RQLite
	OlricServers []string // Connection to namespace Olric
	ConfigPath   string
	PID          int
	StartedAt    time.Time

	Status          InstanceNodeStatus
	LastHealthCheck time.Time
	// contains filtered or unexported fields
}

GatewayInstance represents a running Gateway instance for a namespace

func (*GatewayInstance) DSN

func (gi *GatewayInstance) DSN() string

DSN returns the local connection address for this Gateway instance

func (*GatewayInstance) ExternalURL

func (gi *GatewayInstance) ExternalURL() string

ExternalURL returns the external URL for accessing this namespace's gateway

func (*GatewayInstance) IsHealthy

func (gi *GatewayInstance) IsHealthy(ctx context.Context) (bool, error)

IsHealthy checks if the Gateway instance is healthy

type GatewayYAMLConfig

type GatewayYAMLConfig struct {
	ListenAddr            string            `yaml:"listen_addr"`
	ClientNamespace       string            `yaml:"client_namespace"`
	RQLiteDSN             string            `yaml:"rqlite_dsn"`
	GlobalRQLiteDSN       string            `yaml:"global_rqlite_dsn,omitempty"`
	BootstrapPeers        []string          `yaml:"bootstrap_peers,omitempty"`
	EnableHTTPS           bool              `yaml:"enable_https,omitempty"`
	DomainName            string            `yaml:"domain_name,omitempty"`
	TLSCacheDir           string            `yaml:"tls_cache_dir,omitempty"`
	OlricServers          []string          `yaml:"olric_servers"`
	OlricTimeout          string            `yaml:"olric_timeout,omitempty"`
	IPFSClusterAPIURL     string            `yaml:"ipfs_cluster_api_url,omitempty"`
	IPFSAPIURL            string            `yaml:"ipfs_api_url,omitempty"`
	IPFSTimeout           string            `yaml:"ipfs_timeout,omitempty"`
	IPFSReplicationFactor int               `yaml:"ipfs_replication_factor,omitempty"`
	WebRTC                GatewayYAMLWebRTC `yaml:"webrtc,omitempty"`
}

GatewayYAMLConfig represents the gateway YAML configuration structure This must match the yamlCfg struct in cmd/gateway/config.go exactly because the gateway uses strict YAML decoding that rejects unknown fields

type GatewayYAMLWebRTC

type GatewayYAMLWebRTC struct {
	Enabled    bool   `yaml:"enabled"`
	SFUPort    int    `yaml:"sfu_port,omitempty"`
	TURNDomain string `yaml:"turn_domain,omitempty"`
	TURNSecret string `yaml:"turn_secret,omitempty"`
}

GatewayYAMLWebRTC represents the webrtc section of the gateway YAML config. Must match yamlWebRTCCfg in cmd/gateway/config.go.

type HTTPGateway added in v0.72.0

type HTTPGateway struct {
	// contains filtered or unexported fields
}

HTTPGateway is the main reverse proxy router

func NewHTTPGateway added in v0.72.0

func NewHTTPGateway(logger *logging.ColoredLogger, cfg *config.HTTPGatewayConfig) (*HTTPGateway, error)

NewHTTPGateway creates a new HTTP reverse proxy gateway

func (*HTTPGateway) Router added in v0.72.0

func (hg *HTTPGateway) Router() chi.Router

Router returns the chi router for testing or extension

func (*HTTPGateway) Start added in v0.72.0

func (hg *HTTPGateway) Start(ctx context.Context) error

Start starts the HTTP gateway server

func (*HTTPGateway) Stop added in v0.72.0

func (hg *HTTPGateway) Stop() error

Stop gracefully stops the HTTP gateway server

type HTTPSGateway added in v0.72.0

type HTTPSGateway struct {
	*HTTPGateway
	// contains filtered or unexported fields
}

HTTPSGateway extends HTTPGateway with HTTPS/TLS support

func NewHTTPSGateway added in v0.72.0

func NewHTTPSGateway(logger *logging.ColoredLogger, cfg *config.HTTPGatewayConfig) (*HTTPSGateway, error)

NewHTTPSGateway creates a new HTTPS gateway with Let's Encrypt autocert

func (*HTTPSGateway) Start added in v0.72.0

func (g *HTTPSGateway) Start(ctx context.Context) error

Start starts both HTTP (for ACME) and HTTPS servers

func (*HTTPSGateway) Stop added in v0.72.0

func (g *HTTPSGateway) Stop() error

Stop gracefully stops both HTTP and HTTPS servers

type InstanceConfig

type InstanceConfig struct {
	Namespace       string        // Namespace name (e.g., "alice")
	NodeID          string        // Physical node ID
	HTTPPort        int           // HTTP API port
	BaseDomain      string        // Base domain (e.g., "orama-devnet.network")
	RQLiteDSN       string        // RQLite connection DSN (e.g., "http://localhost:10000")
	GlobalRQLiteDSN string        // Global RQLite DSN for API key validation (empty = use RQLiteDSN)
	OlricServers    []string      // Olric server addresses
	OlricTimeout    time.Duration // Timeout for Olric operations
	NodePeerID      string        // Physical node's peer ID for home node management
	DataDir         string        // Data directory for deployments, SQLite, etc.
	// IPFS configuration for storage endpoints
	IPFSClusterAPIURL     string        // IPFS Cluster API URL (e.g., "http://localhost:9094")
	IPFSAPIURL            string        // IPFS API URL (e.g., "http://localhost:5001")
	IPFSTimeout           time.Duration // Timeout for IPFS operations
	IPFSReplicationFactor int           // IPFS replication factor
	// WebRTC configuration (populated when WebRTC is enabled for the namespace)
	WebRTCEnabled bool   // Enable WebRTC (SFU/TURN) routes on this gateway
	SFUPort       int    // SFU signaling port on this node
	TURNDomain    string // TURN server domain (e.g., "turn.ns-alice.orama-devnet.network")
	TURNSecret    string // TURN shared secret for credential generation
}

InstanceConfig holds configuration for spawning a Gateway instance

type InstanceError

type InstanceError struct {
	Message string
	Cause   error
}

InstanceError represents an error during instance operations (local type to avoid import cycle)

func (*InstanceError) Error

func (e *InstanceError) Error() string

func (*InstanceError) Unwrap

func (e *InstanceError) Unwrap() error

type InstanceNodeStatus

type InstanceNodeStatus string

InstanceNodeStatus represents the status of an instance (local type to avoid import cycle)

const (
	InstanceStatusPending  InstanceNodeStatus = "pending"
	InstanceStatusStarting InstanceNodeStatus = "starting"
	InstanceStatusRunning  InstanceNodeStatus = "running"
	InstanceStatusStopped  InstanceNodeStatus = "stopped"
	InstanceStatusFailed   InstanceNodeStatus = "failed"
)

type InstanceSpawner

type InstanceSpawner struct {
	// contains filtered or unexported fields
}

InstanceSpawner manages multiple Gateway instances for namespace clusters. Each namespace gets its own gateway instances that connect to its dedicated RQLite and Olric clusters.

func NewInstanceSpawner

func NewInstanceSpawner(baseDir string, logger *zap.Logger) *InstanceSpawner

NewInstanceSpawner creates a new Gateway instance spawner

func (*InstanceSpawner) GetInstance

func (is *InstanceSpawner) GetInstance(ns, nodeID string) (*GatewayInstance, bool)

GetInstance returns the instance for a namespace on a specific node

func (*InstanceSpawner) GetNamespaceInstances

func (is *InstanceSpawner) GetNamespaceInstances(ns string) []*GatewayInstance

GetNamespaceInstances returns all instances for a namespace

func (*InstanceSpawner) HealthCheck

func (is *InstanceSpawner) HealthCheck(ctx context.Context, ns, nodeID string) (bool, error)

HealthCheck checks if an instance is healthy

func (*InstanceSpawner) SpawnInstance

func (is *InstanceSpawner) SpawnInstance(ctx context.Context, cfg InstanceConfig) (*GatewayInstance, error)

SpawnInstance starts a new Gateway instance for a namespace on a specific node. Returns the instance info or an error if spawning fails.

func (*InstanceSpawner) StopAllInstances

func (is *InstanceSpawner) StopAllInstances(ctx context.Context, ns string) error

StopAllInstances stops all Gateway instances for a namespace

func (*InstanceSpawner) StopInstance

func (is *InstanceSpawner) StopInstance(ctx context.Context, ns, nodeID string) error

StopInstance stops a Gateway instance for a namespace on a specific node

type NamespaceHealth

type NamespaceHealth struct {
	Status   string                            `json:"status"` // "healthy", "degraded", "unhealthy"
	Services map[string]NamespaceServiceHealth `json:"services"`
}

NamespaceHealth represents the health of a namespace on this node.

type NamespaceRateLimiter

type NamespaceRateLimiter struct {
	// contains filtered or unexported fields
}

NamespaceRateLimiter provides per-namespace rate limiting using a sync.Map for better concurrent performance than a single mutex.

func NewNamespaceRateLimiter

func NewNamespaceRateLimiter(ratePerMinute, burst int) *NamespaceRateLimiter

NewNamespaceRateLimiter creates a per-namespace rate limiter.

func (*NamespaceRateLimiter) Allow

func (nrl *NamespaceRateLimiter) Allow(namespace string) bool

Allow checks if a request for the given namespace should be allowed.

type NamespaceServiceHealth

type NamespaceServiceHealth struct {
	Status  string `json:"status"`
	Port    int    `json:"port"`
	Latency string `json:"latency,omitempty"`
	Error   string `json:"error,omitempty"`
}

NamespaceServiceHealth represents the health of a single namespace service.

type PeerDiscovery

type PeerDiscovery struct {
	// contains filtered or unexported fields
}

PeerDiscovery manages namespace gateway peer discovery via RQLite

func NewPeerDiscovery

func NewPeerDiscovery(h host.Host, rqliteDB *sql.DB, nodeID string, listenPort int, namespace string, logger *zap.Logger) *PeerDiscovery

NewPeerDiscovery creates a new peer discovery manager

func (*PeerDiscovery) Start

func (pd *PeerDiscovery) Start(ctx context.Context) error

Start initializes the peer discovery system

func (*PeerDiscovery) Stop

func (pd *PeerDiscovery) Stop(ctx context.Context) error

Stop stops the peer discovery system

type PresenceMember added in v0.90.0

type PresenceMember struct {
	MemberID string                 `json:"member_id"`
	JoinedAt int64                  `json:"joined_at"` // Unix timestamp
	Meta     map[string]interface{} `json:"meta,omitempty"`
	ConnID   string                 `json:"-"` // Internal: for tracking which connection
}

PresenceMember represents a member in a topic's presence list

type PushNotificationService added in v0.72.0

type PushNotificationService struct {
	// contains filtered or unexported fields
}

PushNotificationService handles sending push notifications via Expo

func NewPushNotificationService added in v0.72.0

func NewPushNotificationService(logger *zap.Logger) *PushNotificationService

NewPushNotificationService creates a new push notification service

func (*PushNotificationService) SendBulkNotifications added in v0.72.0

func (pns *PushNotificationService) SendBulkNotifications(
	ctx context.Context,
	expoPushTokens []string,
	title string,
	body string,
	data map[string]interface{},
	avatarURL string,
) []error

SendBulkNotifications sends notifications to multiple users

func (*PushNotificationService) SendNotification added in v0.72.0

func (pns *PushNotificationService) SendNotification(
	ctx context.Context,
	expoPushToken string,
	title string,
	body string,
	data map[string]interface{},
	avatarURL string,
) error

SendNotification sends a push notification via Expo

type RateLimiter

type RateLimiter struct {
	// contains filtered or unexported fields
}

RateLimiter implements a token-bucket rate limiter per client IP.

func NewRateLimiter

func NewRateLimiter(ratePerMinute, burst int) *RateLimiter

NewRateLimiter creates a rate limiter. ratePerMinute is the sustained rate; burst is the maximum number of requests that can be made in a short window.

func (*RateLimiter) Allow

func (rl *RateLimiter) Allow(ip string) bool

Allow checks if a request from the given IP should be allowed.

func (*RateLimiter) Cleanup

func (rl *RateLimiter) Cleanup(maxAge time.Duration)

Cleanup removes stale entries older than the given duration.

func (*RateLimiter) StartCleanup

func (rl *RateLimiter) StartCleanup(interval, maxAge time.Duration)

StartCleanup runs periodic cleanup in a goroutine. Call Stop() to terminate it.

func (*RateLimiter) Stop

func (rl *RateLimiter) Stop()

Stop terminates the background cleanup goroutine.

type TCPSNIGateway added in v0.72.0

type TCPSNIGateway struct {
	// contains filtered or unexported fields
}

TCPSNIGateway handles SNI-based TCP routing for services like RQLite Raft, IPFS, etc.

func NewTCPSNIGateway added in v0.72.0

func NewTCPSNIGateway(logger *logging.ColoredLogger, cfg *config.SNIConfig) (*TCPSNIGateway, error)

NewTCPSNIGateway creates a new TCP SNI-based gateway

func (*TCPSNIGateway) Start added in v0.72.0

func (g *TCPSNIGateway) Start(ctx context.Context) error

Start starts the TCP SNI gateway server

func (*TCPSNIGateway) Stop added in v0.72.0

func (g *TCPSNIGateway) Stop() error

Stop gracefully stops the TCP SNI gateway

Directories

Path Synopsis
handlers
auth
Package auth provides HTTP handlers for wallet-based authentication, JWT token management, and API key operations.
Package auth provides HTTP handlers for wallet-based authentication, JWT token management, and API key operations.
namespace
Package namespace provides HTTP handlers for namespace cluster operations
Package namespace provides HTTP handlers for namespace cluster operations

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL