config.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. package config
  2. import (
  3. "errors"
  4. "fmt"
  5. "net"
  6. "net/url"
  7. "strings"
  8. "time"
  9. )
  10. var (
  11. // Simple error for duplicate listener definitions
  12. errDuplicateListener = errors.New("duplicate listener definition")
  13. // Simple error for missing BOS listeners
  14. errNoBOSListeners = errors.New("at least one BOS listener is required")
  15. )
  16. // Custom error types for URI-related errors
  17. type uriFormatError struct {
  18. URI string
  19. Err error
  20. }
  21. func (e uriFormatError) Error() string {
  22. return fmt.Sprintf("invalid listener URI %q: %v. Valid format: SCHEME://HOST:PORT (e.g., LOCAL://0.0.0.0:5190)", e.URI, e.Err)
  23. }
  24. type Build struct {
  25. Version string `json:"version"`
  26. Commit string `json:"commit"`
  27. Date string `json:"date"`
  28. }
  29. // ListenerGroup is a set of related BOS endpoints: one plaintext, and
  30. // optionally one for SSL clients. Both listen in plaintext — a load balancer
  31. // terminates TLS and forwards decrypted traffic to the SSL endpoint. Pairing
  32. // them lets a redirect hand a client the sibling endpoint's advertised host,
  33. // so a session can upgrade to SSL or downgrade to plaintext on reconnect.
  34. type ListenerGroup struct {
  35. // Name is the URI scheme the group was parsed from, e.g. "LOCAL".
  36. Name string
  37. BOSListenAddress string
  38. BOSListenAddressSSL string
  39. BOSAdvertisedHostPlain string
  40. BOSAdvertisedHostSSL string
  41. KerberosListenAddress string
  42. }
  43. // HasSSL reports whether clients can reach this group over SSL. Config
  44. // validation guarantees such a group also has an SSL listen address.
  45. func (g ListenerGroup) HasSSL() bool {
  46. return g.BOSAdvertisedHostSSL != ""
  47. }
  48. // PlainEndpoint returns the group's plaintext BOS socket.
  49. func (g ListenerGroup) PlainEndpoint() Endpoint {
  50. return Endpoint{Group: g, ListenAddress: g.BOSListenAddress}
  51. }
  52. // SSLEndpoint returns the socket that receives decrypted traffic from the
  53. // group's SSL terminator. ok is false when SSL is not enabled for the group.
  54. func (g ListenerGroup) SSLEndpoint() (ep Endpoint, ok bool) {
  55. if !g.HasSSL() {
  56. return Endpoint{}, false
  57. }
  58. return Endpoint{Group: g, ListenAddress: g.BOSListenAddressSSL, IsSSL: true}, true
  59. }
  60. // Endpoints returns every BOS socket the group binds.
  61. func (g ListenerGroup) Endpoints() []Endpoint {
  62. eps := []Endpoint{g.PlainEndpoint()}
  63. if ssl, ok := g.SSLEndpoint(); ok {
  64. eps = append(eps, ssl)
  65. }
  66. return eps
  67. }
  68. // Endpoint is a single BOS socket. IsSSL means traffic arrives from an SSL
  69. // terminator, so clients that connect here stay on the SSL path.
  70. type Endpoint struct {
  71. Group ListenerGroup
  72. ListenAddress string
  73. IsSSL bool
  74. }
  75. // AdvertisedHost returns the BOS host clients on this endpoint reconnect to.
  76. func (e Endpoint) AdvertisedHost() string {
  77. if e.IsSSL {
  78. return e.Group.BOSAdvertisedHostSSL
  79. }
  80. return e.Group.BOSAdvertisedHostPlain
  81. }
  82. //go:generate go run ../cmd/config_generator unix settings.env ssl
  83. type Config struct {
  84. BOSListeners []string `envconfig:"OSCAR_LISTENERS" required:"true" basic:"LOCAL://0.0.0.0:5190" ssl:"LOCAL://0.0.0.0:5190" description:"Network listeners for core OSCAR services. For multi-homed servers, allows users to connect from multiple networks. For example, you can allow both LAN and Internet clients to connect to the same server using different connection settings.\n\nFormat:\n\t- Comma-separated list of [NAME]://[HOSTNAME]:[PORT]\n\t- Listener names and ports must be unique\n\t- Listener names are user-defined\n\t- Each listener needs a listener in OSCAR_ADVERTISED_LISTENERS_PLAIN\n\nExamples:\n\t// Listen on all interfaces\n\tLAN://0.0.0.0:5190\n\t// Separate Internet and LAN config\n\tWAN://142.250.176.206:5190,LAN://192.168.1.10:5191"`
  85. BOSAdvertisedHostsPlain []string `envconfig:"OSCAR_ADVERTISED_LISTENERS_PLAIN" required:"true" basic:"LOCAL://127.0.0.1:5190" ssl:"LOCAL://ras.dev:5190" description:"Hostnames published by the server that clients connect to for accessing various OSCAR services. These hostnames are NOT the bind addresses. For multi-homed use servers, allows clients to connect using separate hostnames per network.\n\nFormat:\n\t- Comma-separated list of [NAME]://[HOSTNAME]:[PORT]\n\t- Each listener config must correspond to a config in OSCAR_LISTENERS\n\t- Clients MUST be able to connect to these hostnames\n\nExamples:\n\t// Local LAN config, server behind NAT\n\tLAN://192.168.1.10:5190\n\t// Separate Internet and LAN config\n\tWAN://aim.example.com:5190,LAN://192.168.1.10:5191"`
  86. BOSListenersSSL []string `envconfig:"OSCAR_LISTENERS_SSL" required:"false" basic:"" ssl:"LOCAL://0.0.0.0:5191" description:"Network listeners for core OSCAR services that receive decrypted traffic from an SSL terminator such as nginx. Clients that connect through these listeners are redirected to the hostnames in OSCAR_ADVERTISED_LISTENERS_SSL, keeping them on the SSL path for the rest of the session.\n\nFormat:\n\t- Comma-separated list of [NAME]://[HOSTNAME]:[PORT]\n\t- Listener names and ports must be unique\n\t- Each listener needs a listener in OSCAR_LISTENERS and OSCAR_ADVERTISED_LISTENERS_SSL\n\t- A listener without a matching OSCAR_ADVERTISED_LISTENERS_SSL entry is not started\n\nExamples:\n\t// Listen on all interfaces\n\tLAN://0.0.0.0:5191\n\t// Separate Internet and LAN config\n\tWAN://142.250.176.206:5191,LAN://192.168.1.10:5192"`
  87. BOSAdvertisedHostsSSL []string `envconfig:"OSCAR_ADVERTISED_LISTENERS_SSL" required:"false" basic:"" ssl:"LOCAL://ras.dev:5193" description:"Same as OSCAR_ADVERTISED_LISTENERS_PLAIN, except the hostname is for the server that terminates SSL. Each listener defined here must have a matching listener in OSCAR_LISTENERS_SSL for the terminator to forward decrypted traffic to."`
  88. KerberosListeners []string `envconfig:"KERBEROS_LISTENERS" required:"false" basic:"" ssl:"LOCAL://0.0.0.0:1088" description:"Network listeners for Kerberos authentication. See OSCAR_LISTENERS doc for more details.\n\nExamples:\n\t// Listen on all interfaces\n\tLAN://0.0.0.0:1088\n\t// Separate Internet and LAN config\n\tWAN://142.250.176.206:1088,LAN://192.168.1.10:1087"`
  89. TOCListeners []string `envconfig:"TOC_LISTENERS" required:"true" basic:"0.0.0.0:9898" ssl:"0.0.0.0:9898" description:"Network listeners for TOC protocol service.\n\nFormat: Comma-separated list of hostname:port pairs.\n\nExamples:\n\t// All interfaces\n\t0.0.0.0:9898\n\t// Multiple listeners\n\t0.0.0.0:9898,192.168.1.10:9899"`
  90. APIListener string `envconfig:"API_LISTENER" required:"true" basic:"127.0.0.1:8080" ssl:"127.0.0.1:8080" description:"Network listener for management API binds to. Only 1 listener can be specified. (Default 127.0.0.1 restricts to same machine only)."`
  91. WebAPIListeners []string `envconfig:"WEBAPI_LISTENERS" required:"false" basic:"0.0.0.0:8081" ssl:"0.0.0.0:8081" description:"Network listeners for WebAPI. See OSCAR_LISTENERS doc for more details.\n\nExamples:\n\t// Listen on all interfaces\n\tLAN://0.0.0.0:8081\n\t// Separate Internet and LAN config\n\tWAN://142.250.176.206:8081,LAN://192.168.1.10:8082"`
  92. DBPath string `envconfig:"DB_PATH" required:"true" basic:"oscar.sqlite" ssl:"oscar.sqlite" description:"The path to the SQLite database file. The file and DB schema are auto-created if they doesn't exist."`
  93. DisableAuth bool `envconfig:"DISABLE_AUTH" required:"true" basic:"true" ssl:"true" description:"Disable password check and auto-create new users at login time. Useful for quickly creating new accounts during development without having to register new users via the management API."`
  94. DisableMultiLoginNotif bool `envconfig:"DISABLE_MULTI_LOGIN_NOTIF" required:"false" basic:"true" ssl:"true" description:"Disable notification sent when another client signs in with the same screen name."`
  95. LogLevel string `envconfig:"LOG_LEVEL" required:"true" basic:"info" ssl:"info" description:"Set logging granularity. Possible values: 'trace', 'debug', 'info', 'warn', 'error'."`
  96. // ICQ Legacy Protocol Configuration
  97. ICQLegacy ICQLegacyConfig
  98. }
  99. // ICQLegacyConfig holds configuration for legacy ICQ protocol support (v2-v5)
  100. type ICQLegacyConfig struct {
  101. Enabled bool `envconfig:"ICQ_LEGACY_ENABLED" required:"false" basic:"true" ssl:"true" description:"Enable legacy ICQ protocol support (v2-v5). Allows vintage ICQ clients to connect."`
  102. UDPListener string `envconfig:"ICQ_LEGACY_UDP_LISTENER" required:"false" basic:"0.0.0.0:4000" ssl:"0.0.0.0:4000" description:"UDP listener address for legacy ICQ protocols.\n\nFormat: HOST:PORT\n\nExamples:\n\t// All interfaces\n\t0.0.0.0:4000\n\t// Specific interface\n\t192.168.1.10:4000"`
  103. SupportedVersions []int `envconfig:"ICQ_LEGACY_VERSIONS" required:"false" basic:"2,3,4,5" ssl:"2,3,4,5" description:"Comma-separated list of supported ICQ protocol versions. Valid values: 1, 2, 3, 4, 5 (V1 is experimental)."`
  104. SessionTimeout time.Duration `envconfig:"ICQ_LEGACY_SESSION_TIMEOUT" required:"false" basic:"120s" ssl:"120s" description:"Session timeout for legacy ICQ connections. Sessions are cleaned up after this duration of inactivity."`
  105. KeepAliveInterval time.Duration `envconfig:"ICQ_LEGACY_KEEPALIVE_INTERVAL" required:"false" basic:"120s" ssl:"120s" description:"Expected keep-alive interval from clients. Used for timeout calculations."`
  106. AutoRegistration bool `envconfig:"ICQ_LEGACY_AUTO_REGISTRATION" required:"false" basic:"false" ssl:"false" description:"Allow automatic user registration from legacy clients. When enabled, new UINs can be created via the legacy protocol."`
  107. DepartmentsEnabled bool `envconfig:"ICQ_LEGACY_DEPARTMENTS_ENABLED" required:"false" basic:"false" ssl:"false" description:"Enable department listing feature (groupware functionality)."`
  108. BroadcastEnabled bool `envconfig:"ICQ_LEGACY_BROADCAST_ENABLED" required:"false" basic:"true" ssl:"true" description:"Enable broadcast message functionality."`
  109. WWPEnabled bool `envconfig:"ICQ_LEGACY_WWP_ENABLED" required:"false" basic:"true" ssl:"true" description:"Enable Web Pager (WWP) message support."`
  110. DirectConnections []int `envconfig:"ICQ_LEGACY_DIRECT_CONNECTIONS" required:"false" basic:"5" ssl:"5" description:"Comma-separated list of protocol versions that send real connection info (IP, port) in user online notifications. Disabled for privacy and interoperability. Required for peer-to-peer features (file transfer, direct chat). Example: 5 or 3,4,5"`
  111. }
  112. // DefaultICQLegacyConfig returns the default configuration for ICQ legacy protocol
  113. func DefaultICQLegacyConfig() ICQLegacyConfig {
  114. return ICQLegacyConfig{
  115. Enabled: true,
  116. UDPListener: "0.0.0.0:4000",
  117. SupportedVersions: []int{2, 3, 4, 5},
  118. SessionTimeout: 120 * time.Second,
  119. KeepAliveInterval: 120 * time.Second,
  120. AutoRegistration: false,
  121. DepartmentsEnabled: false,
  122. BroadcastEnabled: true,
  123. WWPEnabled: true,
  124. }
  125. }
  126. // SupportsVersion checks if a specific protocol version is enabled
  127. func (c *ICQLegacyConfig) SupportsVersion(version int) bool {
  128. for _, v := range c.SupportedVersions {
  129. if v == version {
  130. return true
  131. }
  132. }
  133. return false
  134. }
  135. // DirectConnectionEnabled checks if direct connections are enabled for a specific protocol version
  136. func (c *ICQLegacyConfig) DirectConnectionEnabled(version int) bool {
  137. for _, v := range c.DirectConnections {
  138. if v == version {
  139. return true
  140. }
  141. }
  142. return false
  143. }
  144. func (c *Config) ParseListenersCfg() ([]ListenerGroup, error) {
  145. // Helper function to parse and validate a single URI
  146. parseURI := func(uriStr string) (*url.URL, error) {
  147. uriStr = strings.TrimSpace(uriStr)
  148. if uriStr == "" {
  149. return nil, nil
  150. }
  151. u, err := url.Parse(uriStr)
  152. if err != nil {
  153. return nil, uriFormatError{URI: uriStr, Err: err}
  154. }
  155. switch {
  156. case u.Scheme == "":
  157. return nil, uriFormatError{URI: uriStr, Err: errors.New("missing scheme")}
  158. case u.Hostname() == "":
  159. return nil, uriFormatError{URI: uriStr, Err: errors.New("missing host")}
  160. case u.Port() == "":
  161. return nil, uriFormatError{URI: uriStr, Err: errors.New("missing port")}
  162. }
  163. return u, nil
  164. }
  165. m := make(map[string]*ListenerGroup)
  166. // Parse BOS listeners
  167. for _, uriStr := range c.BOSListeners {
  168. u, err := parseURI(uriStr)
  169. if err != nil {
  170. return nil, err
  171. }
  172. if u == nil {
  173. continue
  174. }
  175. if _, ok := m[u.Scheme]; !ok {
  176. m[u.Scheme] = &ListenerGroup{}
  177. }
  178. if m[u.Scheme].BOSListenAddress != "" {
  179. return nil, errDuplicateListener
  180. }
  181. m[u.Scheme].BOSListenAddress = net.JoinHostPort(u.Hostname(), u.Port())
  182. }
  183. // Parse SSL BOS listeners
  184. for _, uriStr := range c.BOSListenersSSL {
  185. u, err := parseURI(uriStr)
  186. if err != nil {
  187. return nil, err
  188. }
  189. if u == nil {
  190. continue
  191. }
  192. if _, ok := m[u.Scheme]; !ok {
  193. m[u.Scheme] = &ListenerGroup{}
  194. }
  195. if m[u.Scheme].BOSListenAddressSSL != "" {
  196. return nil, errDuplicateListener
  197. }
  198. m[u.Scheme].BOSListenAddressSSL = net.JoinHostPort(u.Hostname(), u.Port())
  199. }
  200. // Parse plaintext BOS advertised listeners
  201. for _, uriStr := range c.BOSAdvertisedHostsPlain {
  202. u, err := parseURI(uriStr)
  203. if err != nil {
  204. return nil, err
  205. }
  206. if u == nil {
  207. continue
  208. }
  209. if _, ok := m[u.Scheme]; !ok {
  210. m[u.Scheme] = &ListenerGroup{}
  211. }
  212. if m[u.Scheme].BOSAdvertisedHostPlain != "" {
  213. return nil, errDuplicateListener
  214. }
  215. m[u.Scheme].BOSAdvertisedHostPlain = net.JoinHostPort(u.Hostname(), u.Port())
  216. }
  217. // Parse SSL BOS advertised listeners
  218. for _, uriStr := range c.BOSAdvertisedHostsSSL {
  219. u, err := parseURI(uriStr)
  220. if err != nil {
  221. return nil, err
  222. }
  223. if u == nil {
  224. continue
  225. }
  226. if _, ok := m[u.Scheme]; !ok {
  227. m[u.Scheme] = &ListenerGroup{}
  228. }
  229. if m[u.Scheme].BOSAdvertisedHostSSL != "" {
  230. return nil, errDuplicateListener
  231. }
  232. m[u.Scheme].BOSAdvertisedHostSSL = net.JoinHostPort(u.Hostname(), u.Port())
  233. }
  234. // Parse Kerberos listeners
  235. for _, uriStr := range c.KerberosListeners {
  236. u, err := parseURI(uriStr)
  237. if err != nil {
  238. return nil, err
  239. }
  240. if u == nil {
  241. continue
  242. }
  243. if _, ok := m[u.Scheme]; !ok {
  244. m[u.Scheme] = &ListenerGroup{}
  245. }
  246. if m[u.Scheme].KerberosListenAddress != "" {
  247. return nil, errDuplicateListener
  248. }
  249. m[u.Scheme].KerberosListenAddress = net.JoinHostPort(u.Hostname(), u.Port())
  250. }
  251. ret := make([]ListenerGroup, 0, len(m))
  252. for k, v := range m {
  253. switch {
  254. case v.BOSAdvertisedHostPlain == "":
  255. return nil, fmt.Errorf("missing BOS advertise address for listener `%s://`", k)
  256. case v.BOSListenAddress == "":
  257. return nil, fmt.Errorf("missing BOS listen address for listener `%s://`", k)
  258. case v.HasSSL() && v.BOSListenAddressSSL == "":
  259. return nil, fmt.Errorf("missing SSL BOS listen address for listener `%s://`", k)
  260. }
  261. v.Name = k
  262. ret = append(ret, *v)
  263. }
  264. if len(ret) == 0 {
  265. return nil, errNoBOSListeners
  266. }
  267. // Catch sockets that collide across lists or groups, which would otherwise
  268. // surface at bind time as a bare "address already in use".
  269. seen := make(map[string]string, len(ret)*3)
  270. for _, l := range ret {
  271. for _, socket := range []struct{ envVar, addr string }{
  272. {"OSCAR_LISTENERS", l.BOSListenAddress},
  273. {"OSCAR_LISTENERS_SSL", l.BOSListenAddressSSL},
  274. {"KERBEROS_LISTENERS", l.KerberosListenAddress},
  275. } {
  276. if socket.addr == "" {
  277. continue
  278. }
  279. src := fmt.Sprintf("%s `%s://`", socket.envVar, l.Name)
  280. if prev, ok := seen[socket.addr]; ok {
  281. return nil, fmt.Errorf("listen address %s is configured for both %s and %s", socket.addr, prev, src)
  282. }
  283. seen[socket.addr] = src
  284. }
  285. }
  286. return ret, nil
  287. }
  288. func (c *Config) Validate() error {
  289. // Validate TOCListeners (format: hostname:port pairs)
  290. for _, listener := range c.TOCListeners {
  291. listener = strings.TrimSpace(listener)
  292. if listener == "" {
  293. continue
  294. }
  295. host, port, err := net.SplitHostPort(listener)
  296. if err != nil {
  297. return fmt.Errorf("invalid TOC listener %q: %v. Valid format: HOST:PORT (e.g., 0.0.0.0:9898)", listener, err)
  298. }
  299. if host == "" {
  300. return fmt.Errorf("invalid TOC listener %q: missing host. Valid format: HOST:PORT (e.g., 0.0.0.0:9898)", listener)
  301. }
  302. if port == "" {
  303. return fmt.Errorf("invalid TOC listener %q: missing port. Valid format: HOST:PORT (e.g., 0.0.0.0:9898)", listener)
  304. }
  305. }
  306. // Validate APIListener (format: hostname:port pair, no scheme)
  307. apiListener := strings.TrimSpace(c.APIListener)
  308. if apiListener == "" {
  309. return fmt.Errorf("APIListener is required and cannot be empty")
  310. }
  311. host, port, err := net.SplitHostPort(apiListener)
  312. if err != nil {
  313. return fmt.Errorf("invalid API listener %q: %v. Valid format: HOST:PORT (e.g., 127.0.0.1:8080)", c.APIListener, err)
  314. }
  315. if host == "" {
  316. return fmt.Errorf("invalid API listener %q: missing host. Valid format: HOST:PORT (e.g., 127.0.0.1:8080)", c.APIListener)
  317. }
  318. if port == "" {
  319. return fmt.Errorf("invalid API listener %q: missing port. Valid format: HOST:PORT (e.g., 127.0.0.1:8080)", c.APIListener)
  320. }
  321. // Validate WebAPIListeners (format: hostname:port pairs, no scheme)
  322. for _, listener := range c.WebAPIListeners {
  323. listener = strings.TrimSpace(listener)
  324. if listener == "" {
  325. continue
  326. }
  327. host, port, err := net.SplitHostPort(listener)
  328. if err != nil {
  329. return fmt.Errorf("invalid web API listener %q: %v. Valid format: HOST:PORT (e.g., 0.0.0.0:8081)", listener, err)
  330. }
  331. if host == "" {
  332. return fmt.Errorf("invalid web API listener %q: missing host. Valid format: HOST:PORT (e.g., 0.0.0.0:8081)", listener)
  333. }
  334. if port == "" {
  335. return fmt.Errorf("invalid web API listener %q: missing port. Valid format: HOST:PORT (e.g., 0.0.0.0:8081)", listener)
  336. }
  337. }
  338. return nil
  339. }