config.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  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. type Listener struct {
  30. BOSListenAddress string
  31. BOSAdvertisedHostPlain string
  32. BOSAdvertisedHostSSL string
  33. KerberosListenAddress string
  34. HasSSL bool
  35. }
  36. //go:generate go run ../cmd/config_generator unix settings.env ssl
  37. type Config struct {
  38. 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"`
  39. BOSAdvertisedHostsPlain []string `envconfig:"OSCAR_ADVERTISED_LISTENERS_PLAIN" required:"true" basic:"LOCAL://127.0.0.1:5190" ssl:"LOCAL://127.0.0.1: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"`
  40. 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."`
  41. 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"`
  42. 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"`
  43. 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)."`
  44. 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"`
  45. 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."`
  46. 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."`
  47. 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."`
  48. LogLevel string `envconfig:"LOG_LEVEL" required:"true" basic:"info" ssl:"info" description:"Set logging granularity. Possible values: 'trace', 'debug', 'info', 'warn', 'error'."`
  49. // ICQ Legacy Protocol Configuration
  50. ICQLegacy ICQLegacyConfig
  51. }
  52. // ICQLegacyConfig holds configuration for legacy ICQ protocol support (v2-v5)
  53. type ICQLegacyConfig struct {
  54. 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."`
  55. 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"`
  56. 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)."`
  57. 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."`
  58. 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."`
  59. 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."`
  60. DepartmentsEnabled bool `envconfig:"ICQ_LEGACY_DEPARTMENTS_ENABLED" required:"false" basic:"false" ssl:"false" description:"Enable department listing feature (groupware functionality)."`
  61. BroadcastEnabled bool `envconfig:"ICQ_LEGACY_BROADCAST_ENABLED" required:"false" basic:"true" ssl:"true" description:"Enable broadcast message functionality."`
  62. WWPEnabled bool `envconfig:"ICQ_LEGACY_WWP_ENABLED" required:"false" basic:"true" ssl:"true" description:"Enable Web Pager (WWP) message support."`
  63. 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"`
  64. }
  65. // DefaultICQLegacyConfig returns the default configuration for ICQ legacy protocol
  66. func DefaultICQLegacyConfig() ICQLegacyConfig {
  67. return ICQLegacyConfig{
  68. Enabled: true,
  69. UDPListener: "0.0.0.0:4000",
  70. SupportedVersions: []int{2, 3, 4, 5},
  71. SessionTimeout: 120 * time.Second,
  72. KeepAliveInterval: 120 * time.Second,
  73. AutoRegistration: false,
  74. DepartmentsEnabled: false,
  75. BroadcastEnabled: true,
  76. WWPEnabled: true,
  77. }
  78. }
  79. // SupportsVersion checks if a specific protocol version is enabled
  80. func (c *ICQLegacyConfig) SupportsVersion(version int) bool {
  81. for _, v := range c.SupportedVersions {
  82. if v == version {
  83. return true
  84. }
  85. }
  86. return false
  87. }
  88. // DirectConnectionEnabled checks if direct connections are enabled for a specific protocol version
  89. func (c *ICQLegacyConfig) DirectConnectionEnabled(version int) bool {
  90. for _, v := range c.DirectConnections {
  91. if v == version {
  92. return true
  93. }
  94. }
  95. return false
  96. }
  97. func (c *Config) ParseListenersCfg() ([]Listener, error) {
  98. // Helper function to parse and validate a single URI
  99. parseURI := func(uriStr string) (*url.URL, error) {
  100. uriStr = strings.TrimSpace(uriStr)
  101. if uriStr == "" {
  102. return nil, nil
  103. }
  104. u, err := url.Parse(uriStr)
  105. if err != nil {
  106. return nil, uriFormatError{URI: uriStr, Err: err}
  107. }
  108. switch {
  109. case u.Scheme == "":
  110. return nil, uriFormatError{URI: uriStr, Err: errors.New("missing scheme")}
  111. case u.Hostname() == "":
  112. return nil, uriFormatError{URI: uriStr, Err: errors.New("missing host")}
  113. case u.Port() == "":
  114. return nil, uriFormatError{URI: uriStr, Err: errors.New("missing port")}
  115. }
  116. return u, nil
  117. }
  118. m := make(map[string]*Listener)
  119. // Parse BOS listeners
  120. for _, uriStr := range c.BOSListeners {
  121. u, err := parseURI(uriStr)
  122. if err != nil {
  123. return nil, err
  124. }
  125. if u == nil {
  126. continue
  127. }
  128. if _, ok := m[u.Scheme]; !ok {
  129. m[u.Scheme] = &Listener{}
  130. }
  131. if m[u.Scheme].BOSListenAddress != "" {
  132. return nil, errDuplicateListener
  133. }
  134. m[u.Scheme].BOSListenAddress = net.JoinHostPort(u.Hostname(), u.Port())
  135. }
  136. // Parse plaintext BOS advertised listeners
  137. for _, uriStr := range c.BOSAdvertisedHostsPlain {
  138. u, err := parseURI(uriStr)
  139. if err != nil {
  140. return nil, err
  141. }
  142. if u == nil {
  143. continue
  144. }
  145. if _, ok := m[u.Scheme]; !ok {
  146. m[u.Scheme] = &Listener{}
  147. }
  148. if m[u.Scheme].BOSAdvertisedHostPlain != "" {
  149. return nil, errDuplicateListener
  150. }
  151. m[u.Scheme].BOSAdvertisedHostPlain = net.JoinHostPort(u.Hostname(), u.Port())
  152. }
  153. // Parse SSL BOS advertised listeners
  154. for _, uriStr := range c.BOSAdvertisedHostsSSL {
  155. u, err := parseURI(uriStr)
  156. if err != nil {
  157. return nil, err
  158. }
  159. if u == nil {
  160. continue
  161. }
  162. if _, ok := m[u.Scheme]; !ok {
  163. m[u.Scheme] = &Listener{}
  164. }
  165. if m[u.Scheme].BOSAdvertisedHostSSL != "" {
  166. return nil, errDuplicateListener
  167. }
  168. m[u.Scheme].HasSSL = true
  169. m[u.Scheme].BOSAdvertisedHostSSL = net.JoinHostPort(u.Hostname(), u.Port())
  170. }
  171. // Parse Kerberos listeners
  172. for _, uriStr := range c.KerberosListeners {
  173. u, err := parseURI(uriStr)
  174. if err != nil {
  175. return nil, err
  176. }
  177. if u == nil {
  178. continue
  179. }
  180. if _, ok := m[u.Scheme]; !ok {
  181. m[u.Scheme] = &Listener{}
  182. }
  183. if m[u.Scheme].KerberosListenAddress != "" {
  184. return nil, errDuplicateListener
  185. }
  186. m[u.Scheme].KerberosListenAddress = net.JoinHostPort(u.Hostname(), u.Port())
  187. }
  188. ret := make([]Listener, 0, len(m))
  189. for k, v := range m {
  190. switch {
  191. case v.BOSAdvertisedHostPlain == "":
  192. return nil, fmt.Errorf("missing BOS advertise address for listener `%s://`", k)
  193. case v.BOSListenAddress == "":
  194. return nil, fmt.Errorf("missing BOS listen address for listener `%s://`", k)
  195. }
  196. ret = append(ret, *v)
  197. }
  198. if len(ret) == 0 {
  199. return nil, errNoBOSListeners
  200. }
  201. return ret, nil
  202. }
  203. func (c *Config) Validate() error {
  204. // Validate TOCListeners (format: hostname:port pairs)
  205. for _, listener := range c.TOCListeners {
  206. listener = strings.TrimSpace(listener)
  207. if listener == "" {
  208. continue
  209. }
  210. host, port, err := net.SplitHostPort(listener)
  211. if err != nil {
  212. return fmt.Errorf("invalid TOC listener %q: %v. Valid format: HOST:PORT (e.g., 0.0.0.0:9898)", listener, err)
  213. }
  214. if host == "" {
  215. return fmt.Errorf("invalid TOC listener %q: missing host. Valid format: HOST:PORT (e.g., 0.0.0.0:9898)", listener)
  216. }
  217. if port == "" {
  218. return fmt.Errorf("invalid TOC listener %q: missing port. Valid format: HOST:PORT (e.g., 0.0.0.0:9898)", listener)
  219. }
  220. }
  221. // Validate APIListener (format: hostname:port pair, no scheme)
  222. apiListener := strings.TrimSpace(c.APIListener)
  223. if apiListener == "" {
  224. return fmt.Errorf("APIListener is required and cannot be empty")
  225. }
  226. host, port, err := net.SplitHostPort(apiListener)
  227. if err != nil {
  228. return fmt.Errorf("invalid API listener %q: %v. Valid format: HOST:PORT (e.g., 127.0.0.1:8080)", c.APIListener, err)
  229. }
  230. if host == "" {
  231. return fmt.Errorf("invalid API listener %q: missing host. Valid format: HOST:PORT (e.g., 127.0.0.1:8080)", c.APIListener)
  232. }
  233. if port == "" {
  234. return fmt.Errorf("invalid API listener %q: missing port. Valid format: HOST:PORT (e.g., 127.0.0.1:8080)", c.APIListener)
  235. }
  236. // Validate WebAPIListeners (format: hostname:port pairs, no scheme)
  237. for _, listener := range c.WebAPIListeners {
  238. listener = strings.TrimSpace(listener)
  239. if listener == "" {
  240. continue
  241. }
  242. host, port, err := net.SplitHostPort(listener)
  243. if err != nil {
  244. return fmt.Errorf("invalid web API listener %q: %v. Valid format: HOST:PORT (e.g., 0.0.0.0:8081)", listener, err)
  245. }
  246. if host == "" {
  247. return fmt.Errorf("invalid web API listener %q: missing host. Valid format: HOST:PORT (e.g., 0.0.0.0:8081)", listener)
  248. }
  249. if port == "" {
  250. return fmt.Errorf("invalid web API listener %q: missing port. Valid format: HOST:PORT (e.g., 0.0.0.0:8081)", listener)
  251. }
  252. }
  253. return nil
  254. }