ldapper

package module
v0.0.0-...-9a7898c Latest Latest
Warning

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

Go to latest
Published: Oct 30, 2026 License: MIT Imports: 8 Imported by: 0

README

LDAPper

Go Reference

A lightweight, adapter-based LDAP client for Go. LDAPper wraps go-ldap/ldap with:

  • Multi-host failover and transparent reconnection.
  • Pluggable catalog adapters so the same API works against Microsoft Active Directory, OpenLDAP, ALD Pro, FreeIPA, and any other directory you write an adapter for.
  • Connection-level search limits so callers don't pass SizeLimit / TimeLimit on every query.
  • Efficient catalog tree discovery — a single paged subtree search, assembled into an OU tree on the client, with optional depth trimming.

Table of contents


Installation

go get github.com/NGRsoftlab/ldapper

Requires Go 1.21+ (module tested against Go 1.26). Depends on github.com/go-ldap/ldap/v3.


Quick start

package main

import (
	"crypto/tls"
	"log"

	"github.com/NGRsoftlab/ldapper"
	"github.com/NGRsoftlab/ldapper/adapters"
)

func main() {
	adapter := adapters.MSADAdapter{}

	conn, user, err := ldapper.NewAuthenticatedConnection(
		"svc-reader@corp.example.com", "secret", // bind credentials
		"DC=corp,DC=example,DC=com",             // base DN for user search
		[]string{"dc1.corp.example.com", "dc2.corp.example.com"}, // hosts
		636,        // port
		true,       // useSSL (LDAPS)
		false,      // skipTLS (irrelevant when useSSL is true)
		&tls.Config{MinVersion: tls.VersionTLS12},
		adapter,
		ldapper.WithSearchLimits(0, 30), // no size limit, 30s time limit
	)
	if err != nil {
		log.Fatalf("connect: %v", err)
	}
	defer conn.Close()

	log.Printf("authenticated as %s", user.DN)

	// Fetch a user by sAMAccountName.
	u, err := conn.GetUser("jdoe", "DC=corp,DC=example,DC=com")
	if err != nil {
		log.Fatalf("get user: %v", err)
	}

	info := adapter.GetNormalizedUserInfo(*u)
	log.Printf("%s <%s>", info.CommonName, info.Mail)
}

Architecture

LDAPper is intentionally small and layered. Only two things are pluggable (interfaces.LDAPConn and interfaces.CatalogAdapter); everything else is plain structs and functions.

Layers
┌──────────────────────────────────────────────────────────────────┐
│  User code                                                       │
│    conn, _ := ldapper.NewAuthenticatedConnection(...)            │
│    conn.GetUser(...)  /  conn.GetCatalogTree(...)  /  ...        │
└──────────────────────────────┬───────────────────────────────────┘
                               │
┌──────────────────────────────▼───────────────────────────────────┐
│  LDAPper — public API                                            │
│                                                                  │
│  ldapper.go                        connection lifecycle:        │
│                                    dial / failover, auth,        │
│                                    IsAlive / Reconnect / Close   │
│  ldapper_search.go                 user / group / OU-user search │
│  ldapper_search_params.go          SearchLimits + newSearchRequest│
│  ldapper_catalog_tree_discovery.go paged subtree + tree builder  │
└──────────────┬───────────────────────────────────┬───────────────┘
               │                                   │
     uses      │                                   │  uses
               ▼                                   ▼
┌──────────────────────────────┐    ┌──────────────────────────────┐
│  interfaces.LDAPConn         │    │  interfaces.CatalogAdapter   │
│  (thin subset of go-ldap)    │    │  (schema-specific glue)      │
│                              │    │                              │
│  StartTLS / Bind / Search /  │    │  Filters / Attrs / Casts     │
│  IsClosing / Close           │    │                              │
└──────────────┬───────────────┘    └──────────────┬───────────────┘
               │                                   │
   prod: *ldap.Conn                  prod: adapters.{MSAD, OPENLDAP,
   test: mocks.MockLDAPConn                ALDRPO, FREEIPA}Adapter
                                     test: mocks.MockCatalogAdapter
                                           or testCatalogAdapter

                               │
┌──────────────────────────────▼───────────────────────────────────┐
│  go-ldap/ldap/v3                                                 │
└──────────────────────────────┬───────────────────────────────────┘
                               │
                          LDAP server

The two interfaces are the only seams:

  • interfaces.LDAPConn is a 5-method subset of *ldap.Conn. Production uses the real *ldap.Conn; tests inject mocks.MockLDAPConn.
  • interfaces.CatalogAdapter is the only place LDAPper knows anything about directory schema. All filters, attribute lists, and value casts go through it.
LDAPper struct
LDAPper
├── catalogAdapter  interfaces.CatalogAdapter   schema-specific behavior
├── connection      interfaces.LDAPConn         active network connection
│
├── isAuthenticated bool                        set by AuthenticateConn
├── bindUser        string                      stored for Reconnect re-bind
├── bindPassword    string
│
├── hosts           []string                    failover list (≤ 50)
├── currentHost     string                      host of the active connection
├── port            uint
│
├── useSSL          bool                        LDAPS vs plain LDAP
├── skipTLS         bool                        disable StartTLS upgrade
├── tlsCfg          *tls.Config
│
└── searchLimits    SearchLimits                injected via newSearchRequest

Every search method goes through newSearchRequest(...), which injects searchLimits. Changing limits at runtime (SetSearchLimits) is therefore a single assignment with no plumbing.

Search flow (GetUser as example)
conn.GetUser("jdoe", baseDN)
        │
        ├── catalogAdapter.SearchUserFilterTemplate()
        │       → "(uid=%s)" → fmt.Sprintf → "(uid=jdoe)"
        ├── catalogAdapter.UserAttrs()
        │       → []string{"cn", "mail", ...}
        │
        ▼
   newSearchRequest(baseDN, WholeSubtree, NeverDerefAliases,
                    false, filter, attrs, nil)
        │
        │  injects conn.searchLimits.{SizeLimit, TimeLimit}
        ▼
   conn.connection.Search(req)  ─────►  LDAP server
        │
        ▼
   0 entries   → error "user does not exist"
   1 entry     → build types.LDAPUser{DN, Attributes}
   >1 entries  → error "too many entries returned"
Catalog tree flow (GetCatalogTree)
conn.GetCatalogTree(baseDN, maxDepth)
        │
        ▼
   loop:
     Search(baseDN, WholeSubtree, paging=1000)
       │
       ├── server echoes ControlPaging  →  read cookie
       │
       ▼
     next request with cookie  …  until empty cookie
        │
        ▼
   allEntries []*ldap.Entry
        │
        ▼
   for each entry:
       catalogAdapter.OUInfoFromEntry(entry)
       drop if !baseDN.AncestorOf(entry.DN)
       byDN[info.DN] = info
        │
        ▼
   attach each node to parentDNOf(info.DN)  →  tree roots
        │
        ▼
   if maxDepth > 0:  trimDepth(tree, maxDepth)
        │
        ▼
   types.CatalogTree{Tree: [...]}

This is the whole reason GetCatalogTree is O(1) round-trips instead of O(N): the server streams pages of the subtree, and the parent-child wiring is done locally in O(N) with a single map[DN]*OUInfo.

Repository layout
ldapper/
├── ldapper.go                        constructors + connection lifecycle
├── ldapper_search.go                 CustomSearch / GetUser / GetUserGroups / GetOUUsers
├── ldapper_search_params.go          SearchLimits + newSearchRequest
├── ldapper_catalog_tree_discovery.go GetCatalogTree / GetRootOUs / GetSubOUs
│
├── interfaces/
│   ├── catalog_adapter_interface.go  interfaces.CatalogAdapter
│   └── ldap_native_conn_interface.go interfaces.LDAPConn
│
├── adapters/
│   ├── msad_adapter.go               Microsoft Active Directory
│   ├── openldap_adapter.go           OpenLDAP / RFC-4519
│   ├── aldpro_adapter.go             ALD Pro
│   └── freeipa_adapter.go            FreeIPA
│
├── types/
│   ├── ldap_user_type.go             LDAPUser + attribute helpers
│   ├── ldap_catalog_structure_type.go OUInfo / CatalogTree
│   └── normalized_user_type.go       UserNormalizedInfo
│
├── constants/constants.go            all defaults in one place
│
└── mocks/
    ├── ldap_native_conn_mock.go      MockLDAPConn (function-field style)
    └── catalog_adapter_mock.go       MockCatalogAdapter (testify)

Core concepts

Connections

LDAPper is created with one of two constructors:

// Unauthenticated: no bind performed, suitable for directories that allow
// anonymous reads or when you only need to dial.
NewConnection(hosts []string, port uint, useSSL, skipTLS bool,
              tlsCfg *tls.Config, adapter interfaces.CatalogAdapter,
              opts ...Option) (*LDAPper, error)

// Authenticated: dials, then binds as `username` and looks the user up
// via the adapter's user filter to confirm the credentials.
NewAuthenticatedConnection(username, password, baseDN string,
                           hosts []string, port uint,
                           useSSL, skipTLS bool, tlsCfg *tls.Config,
                           adapter interfaces.CatalogAdapter,
                           opts ...Option) (*LDAPper, *types.LDAPUser, error)

Highlights:

  • Host failover — hosts are tried in order until one accepts the connection. The maximum list length is constants.DefaultMaxHostsLength (50) — a guard against accidentally passing a million hosts.
  • TLS — either native LDAPS (useSSL = true) or StartTLS (skipTLS = false). A non-nil *tls.Config is required whenever TLS is used.
  • dialURL is a package-level variable (ldap.DialURL) so tests can swap it out via withMockDialURL(...) without touching production code.
Adapters

All directory-specific behavior lives in an adapter — a value that implements interfaces.CatalogAdapter:

type CatalogAdapter interface {
    // Cast / convert
    GetNormalizedUserInfo(ldapUser types.LDAPUser) *types.UserNormalizedInfo
    OUInfoFromEntry(entry ldap.Entry) *types.OUInfo

    // Object-class filters
    UserObjectFilter()  string
    GroupObjectFilter() string
    OUObjectFilter()    string

    // Search filter templates
    SearchUserFilterTemplate()       string
    SearchUserGroupsFilterTemplate() string
    SearchUserGroupsFilterTemplateArgs(ldapUser types.LDAPUser) []any
    SearchOUFilterTemplate()    string
    SearchGroupFilterTemplate() string

    // Attribute lists
    UserAttrs()  []string
    OUAttrs()    []string
    GroupAttrs() []string
}

The adapter is the only place LDAPper assumes anything about schema. See Shipped adapters for the four adapters that ship with the library.

Search limits

Every search request is built by a single helper that applies connection-level SearchLimits{SizeLimit, TimeLimit}. Callers never pass these per call:

conn, _ := ldapper.NewConnection(..., ldapper.WithSearchLimits(500, 30))

// Later, temporarily raise limits for one heavy pass:
old := conn.GetSearchLimits()
conn.SetSearchLimits(0, 120)
tree, _ := conn.GetCatalogTree("DC=corp,DC=example,DC=com", 0)
conn.SetSearchLimits(old.SizeLimit, old.TimeLimit)
  • SizeLimit == 0 means "server default / no client limit".
  • TimeLimit == 0 means "server default / no client limit".
  • Negative values are clamped to 0.
  • SetSearchLimits is not concurrency-safe — do not call it while searches are in flight.

Shipped adapters

Adapter Directory Notes
adapters.MSADAdapter Microsoft Active Directory Uses objectCategory-based filters; recursive group search via the LDAP_MATCHING_RULE_IN_CHAIN OID (1.2.840.113556.1.4.1941); prefers thumbnailPhoto over jpegPhoto.
adapters.OPENLDAPAdapter OpenLDAP (and most RFC-4519 dialects) Handles both groupOfNames and posixGroup; user search keyed by uid or cn; address mapped from postalAddress.
adapters.ALDRPOAdapter ALD Pro Recognizes rbta-org-unit alongside organizationalUnit; department mapped from rbtadp; supports groupOfNames, posixGroup, ipausergroup.
adapters.FREEIPAAdapter FreeIPA Recognizes nsContainer alongside organizationalUnit; department intentionally not mapped (FreeIPA schemas vary); uses FreeIPA-specific attrs nsAccountLock and ipaUniqueID.
Adapter-specific gotchas
  • MSADAdapter — the base DN for search must be a valid AD naming context. GetUser matches on sAMAccountName; the bind DN in NewAuthenticatedConnection can be a UPN (user@domain), not necessarily a full DN.
  • OPENLDAPAdapter — the user filter uses the indexed format placeholder %[1]s so the same argument is substituted into both uid= and cn=. Computer objects are explicitly excluded.
  • ALDRPOAdapter / FREEIPAAdapter — the user-groups filter template is member=uid=%s, which expects the short uid (not a DN). SearchUserGroupsFilterTemplateArgs returns exactly one argument to match the single placeholder. If your directory stores full DNs in member, override SearchUserGroupsFilterTemplate in a custom adapter.

All four adapters are stateless and safe to reuse across connections.

Attribute mapping in GetNormalizedUserInfo

Each adapter maps the same UserNormalizedInfo fields from different LDAP attributes. — means the field is hardcoded to empty in that adapter (the attribute is not read even if present).

UserNormalizedInfo field MSAD OpenLDAP ALD Pro FreeIPA
DistinguishedName distinguishedName dn dn dn
CommonName cn cn cn cn
Mobile mobile mobile mobile mobile
Phone telephoneNumber telephoneNumber telephoneNumber telephoneNumber
Mail mail mail mail mail
Department department departmentNumber rbtadp —
Title title title title title
Manager manager manager manager manager
Photo thumbnailPhoto → jpegPhoto jpegPhoto jpegPhoto jpegPhoto
Company company — — —
Country co — co co
Address streetAddress postalAddress streetAddress street
City l — l l
Index postalCode — postalCode postalCode

Key differences to keep in mind when picking an adapter:

  • DistinguishedName — MSAD reads it from the distinguishedName attribute, all other adapters read it from dn.
  • Department — four different sources (department, departmentNumber, rbtadp, or none). FreeIPA simply does not map it.
  • Address — MSAD and ALD Pro use streetAddress, OpenLDAP uses postalAddress, FreeIPA uses street.
  • Photo — only MSAD falls back from thumbnailPhoto to jpegPhoto; the other three adapters read jpegPhoto only and do not consult thumbnailPhoto.
  • Company / Country / City / Index — OpenLDAP hardcodes Company, Country, City and Index to empty regardless of the entry's attributes. If you need them populated against OpenLDAP, write a custom adapter that overrides GetNormalizedUserInfo.

API overview

Authentication
// Bind as `username` and store the credentials for later re-binds.
user, err := conn.AuthenticateConn(username, password, baseDN)

// Check a user's password against an already-authenticated connection,
// then rebind back to the service account.
user, err := conn.AuthenticateUser(username, password, baseDN)

Notes:

  • AuthenticateUser requires the connection to have been created via NewAuthenticatedConnection (or a prior AuthenticateConn). The connection must have read access to look the user up before binding as them.
  • Error messages intentionally do not echo the underlying LDAP error text, to avoid leaking credentials in logs.
Searching
// Escape hatch: build any *ldap.SearchRequest yourself.
res, err := conn.CustomSearch(&ldap.SearchRequest{...})

// Fetch a single user by the adapter's user filter template.
user, err := conn.GetUser("jdoe", baseDN)

// Fetch the CNs of all groups a user belongs to.
groups, err := conn.GetUserGroups("jdoe", baseDN)

// Fetch every user under an OU (subtree search, adapter user filter).
users, err := conn.GetOUUsers("OU=Staff,DC=corp,DC=example,DC=com")

GetUser returns an error when the filter matches zero or more than one entry — user lookups are assumed to be unique.

Catalog tree discovery
tree, err := conn.GetCatalogTree("DC=corp,DC=example,DC=com", 2)
  • Performs one paged subtree search (ControlPaging) and builds the OU tree on the client, instead of issuing one round-trip per level.
  • Filters out entries that are not strict descendants of baseDn (via ldap.DN.AncestorOf) — misbehaving servers and cross-tree referrals can't leak foreign entries into the result.
  • Skips malformed DNs returned by the server rather than aborting.
  • maxDepth trims the resulting tree: 0 = no limit, 1 = roots only, 2 = roots + first level, etc.
// Only root-level OUs (single-level scope, one round-trip):
roots, err := conn.GetRootOUs("DC=corp,DC=example,DC=com")

// Direct children of an OU:
children, err := conn.GetSubOUs("OU=Staff,DC=corp,DC=example,DC=com",
                                ldap.ScopeSingleLevel)

GetCatalogTree is the recommended path for large catalogs: it round-trips once per page, streams results page-by-page, and assembles the tree in O(N) using a map[DN]*OUInfo + parent-DN walk.

Health checks & reconnection
if !conn.IsAlive() {
    _ = conn.Reconnect()
}

// Or the combined form, suitable as a pre-flight check:
if err := conn.EnsureAlive(); err != nil {
    return err
}
  • IsAlive checks the local connection state (IsClosing) and issues a cheap root-DSE search with hard-coded SizeLimit=1, TimeLimit=1. It is intentionally independent of the user-configured search limits.
  • Reconnect closes the current connection, dials any host in hosts again, and — if the connection was authenticated — re-binds with the stored credentials.
  • Close is idempotent and swallows Close errors (best-effort).

Writing your own adapter

Implement interfaces.CatalogAdapter for your directory. The adapter is stateless and safe to reuse across connections. Example skeleton:

type MyAdapter struct{}

func (MyAdapter) GetNormalizedUserInfo(u types.LDAPUser) *types.UserNormalizedInfo {
    return &types.UserNormalizedInfo{
        DistinguishedName: u.DN,
        CommonName:        u.GetAttributeValue("cn"),
        Mail:              u.GetAttributeValue("mail"),
    }
}

func (MyAdapter) OUInfoFromEntry(e ldap.Entry) *types.OUInfo {
    return &types.OUInfo{
        DN:       e.DN,
        Name:     e.GetAttributeValue("ou"),
        Children: make([]*types.OUInfo, 0),
    }
}

func (MyAdapter) UserObjectFilter() string { return "(&(objectClass=inetOrgPerson))" }
func (MyAdapter) UserAttrs() []string      { return []string{"cn", "mail"} }
// ... remaining methods

See adapters/msad_adapter.go, adapters/openldap_adapter.go, adapters/aldpro_adapter.go and adapters/freeipa_adapter.go for complete, production-quality examples.


Configuration & tuning

Defaults live in constants/constants.go:

Constant Default Meaning
LDAPURITemplate "ldap://%s:%d" Used for plain / StartTLS dials
LDAPSURITemplate "ldaps://%s:%d" Used for LDAPS dials
DefaultSearchSizeLimit 0 0 = no client-side size limit
DefaultSearchTimeLimit 30 Seconds
DefaultMaxHostsLength 50 Upper bound on the hosts slice
DefaultCatalogTreePageSize 1000 Paging control size for GetCatalogTree

Options:

Option Effect
WithSearchLimits(size, time) Overrides the connection-level search limits
SetSearchLimits(size, time) Runtime override (not concurrency-safe)
GetSearchLimits() Reads current limits

Testing

The package is designed for testability:

  • dialURL is a package-level var — tests can substitute it with withMockDialURL(t, fn) (see helpers_test.go).

  • interfaces.LDAPConn and interfaces.CatalogAdapter can both be mocked. Ready-made fakes live in mocks/:

    • mocks.MockLDAPConn — BindFunc, SearchFunc, CloseFunc, IsClosingFunc, StartTLSFunc fields.
    • mocks.MockCatalogAdapter — testify-based catalog adapter.
  • Integration tests against an in-memory directory are kept behind //go:build integration (see ldapper_integration_test.go) and can be run with:

    go test -tags=integration ./...
    

Run the unit tests:

go test ./...

Run with the race detector:

go test -race ./...

Tip: StartTLS on a zero-value *ldap.Conn returned by a mocked dialURL blocks on a nil channel. When mocking dialURL, always set skipTLS = true or useSSL = true on the LDAPper under test (see newDialableLDAPper in helpers_test.go).


License

Copyright © 2020–2026 NGR Softlab. See the LICENSE file in the repository root for the full license text.

Documentation

Overview

Copyright 2020-2026 NGR Softlab

Copyright 2020-2026 NGR Softlab

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type LDAPper

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

func NewAuthenticatedConnection

func NewAuthenticatedConnection(
	username, password, baseDN string,
	hosts []string,
	port uint,
	useSSL bool,
	skipTLS bool,
	tlsCfg *tls.Config,
	catalogAdapter interfaces.CatalogAdapter,
	opts ...Option) (*LDAPper, *types.LDAPUser, error)

TODO: pass controlable ctx into connection creator to break dial if smth is not ok (or break by timeout) NB: if you need connection for futher catalog reading - pass read access granted user credentials here NB: suports many host alternatives, max hosts lenght = constants.DefaultMaxHostsLength (need it for 1mln same hosts protection, etc.) NB: use WithSearchLimits(...) to override default size/time limits for all search requests.

func NewConnection

func NewConnection(
	hosts []string,
	port uint,
	useSSL bool,
	skipTLS bool,
	tlsCfg *tls.Config,
	catalogAdapter interfaces.CatalogAdapter,
	opts ...Option) (*LDAPper, error)

TODO: pass controlable ctx into connection creator to break dial if smth is not ok (or break by timeout) NB: suports many host alternatives, max hosts lenght = constants.DefaultMaxHostsLength (need it for 1mln same hosts protection, etc.) NB: use WithSearchLimits(...) to override default size/time limits for all search requests.

func (*LDAPper) AuthenticateConn

func (conn *LDAPper) AuthenticateConn(username, password, baseDN string) (*types.LDAPUser, error)

NB: if you need connection for futher catalog reading - pass read access granted user credentials here

func (*LDAPper) AuthenticateUser

func (conn *LDAPper) AuthenticateUser(username, password, baseDN string) (*types.LDAPUser, error)

NB: can not be used with non-authenticated conn or with non-read-access-granted bindUser in conn cfg

func (*LDAPper) Close

func (conn *LDAPper) Close()

func (*LDAPper) CustomSearch

func (conn *LDAPper) CustomSearch(searchRequest *ldap.SearchRequest) (*ldap.SearchResult, error)

func (*LDAPper) EnsureAlive

func (conn *LDAPper) EnsureAlive() error

EnsureAlive checks liveness and reconnects when the connection is stale. Can be used by callers before performing operations on the connection.

func (*LDAPper) GetCatalogTree

func (conn *LDAPper) GetCatalogTree(baseDn string, maxDepth int) (types.CatalogTree, error)

GetCatalogTree fetches the whole OU subtree in a single LDAP search and builds the tree on the client, optionally trimming to maxDepth (0 = no limit).

func (*LDAPper) GetOUUsers

func (conn *LDAPper) GetOUUsers(ou string) ([]*types.LDAPUser, error)

func (*LDAPper) GetRootOUs

func (conn *LDAPper) GetRootOUs(rootDN string) ([]*types.OUInfo, error)

func (*LDAPper) GetSearchLimits

func (conn *LDAPper) GetSearchLimits() SearchLimits

GetSearchLimits returns current connection-level search limits.

func (*LDAPper) GetSubOUs

func (conn *LDAPper) GetSubOUs(group string, level int) (res []*types.OUInfo, err error)

func (*LDAPper) GetUser

func (conn *LDAPper) GetUser(username, baseDN string) (*types.LDAPUser, error)

func (*LDAPper) GetUserGroups

func (conn *LDAPper) GetUserGroups(username, baseDN string) ([]string, error)

func (*LDAPper) IsAlive

func (conn *LDAPper) IsAlive() bool

IsAlive reports whether the underlying LDAP connection is still usable. It first checks the local state and then performs a lightweight round-trip by querying the root DSE (works for every LDAP server and does not require auth)

func (*LDAPper) Reconnect

func (conn *LDAPper) Reconnect() error

Reconnect closes the current connection and tries to establish a new one against any of the configured hosts. If the previous connection was authenticated, the stored bind credentials are re-applied.

func (*LDAPper) SetSearchLimits

func (conn *LDAPper) SetSearchLimits(sizeLimit, timeLimit int)

SetSearchLimits overrides connection-level search limits at runtime. Negative values are clamped to 0 ("no limit"). NB: not concurrency-safe — do not call while searches are in flight.

type Option

type Option func(*LDAPper)

Option configures optional LDAPper parameters.

func WithSearchLimits

func WithSearchLimits(sizeLimit, timeLimit int) Option

WithSearchLimits overrides default search size/time limits applied to every search request built by the connection. Negative values are clamped to 0 ("no limit").

type SearchLimits

type SearchLimits struct {
	SizeLimit int
	TimeLimit int
}

SearchLimits holds default LDAP search request limits. SizeLimit == 0 means "no size limit" (server default). TimeLimit == 0 means "no time limit" (server default).

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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