config.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. package config
  2. import (
  3. "errors"
  4. "fmt"
  5. "net"
  6. "net/url"
  7. "strings"
  8. "time"
  9. "golang.org/x/text/encoding"
  10. "golang.org/x/text/encoding/charmap"
  11. )
  12. var (
  13. // Simple error for duplicate listener definitions
  14. errDuplicateListener = errors.New("duplicate listener definition")
  15. // Simple error for missing BOS listeners
  16. errNoBOSListeners = errors.New("at least one BOS listener is required")
  17. )
  18. // Custom error types for URI-related errors
  19. type uriFormatError struct {
  20. URI string
  21. Err error
  22. }
  23. func (e uriFormatError) Error() string {
  24. 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)
  25. }
  26. type Build struct {
  27. Version string `json:"version"`
  28. Commit string `json:"commit"`
  29. Date string `json:"date"`
  30. }
  31. func validateAllowedOrigin(origin string) error {
  32. const format = "Valid format: SCHEME://HOST[:PORT] with at most one * (e.g., https://example.com, http://localhost:*)"
  33. reject := func(reason string) error {
  34. return fmt.Errorf("invalid web API allowed origin %q: %s. %s", origin, reason, format)
  35. }
  36. if strings.Count(origin, "*") > 1 {
  37. return reject("only one wildcard is allowed per origin")
  38. }
  39. scheme, host, ok := cutOriginScheme(origin)
  40. if !ok {
  41. return reject("scheme must be http or https, and cannot itself be a wildcard")
  42. }
  43. if host == "" {
  44. return reject("missing host")
  45. }
  46. // An origin is a scheme, host and port and nothing else.
  47. if strings.ContainsAny(host, "/?#@") {
  48. return reject("must not have a trailing slash, path, query, fragment or userinfo")
  49. }
  50. // A browser omits the port when it is the default for the scheme, so an
  51. // entry that spells it out can never match.
  52. if (scheme == "http" && strings.HasSuffix(host, ":80")) ||
  53. (scheme == "https" && strings.HasSuffix(host, ":443")) {
  54. return fmt.Errorf("invalid web API allowed origin %q: a browser omits the default port, so this would never match. Use %q", origin, origin[:strings.LastIndex(origin, ":")])
  55. }
  56. return nil
  57. }
  58. // cutOriginScheme splits an origin into its scheme and the rest.
  59. func cutOriginScheme(origin string) (scheme string, rest string, ok bool) {
  60. lower := strings.ToLower(origin)
  61. for _, s := range []string{"http", "https"} {
  62. if prefix := s + "://"; strings.HasPrefix(lower, prefix) {
  63. return s, origin[len(prefix):], true
  64. }
  65. }
  66. return "", "", false
  67. }
  68. // ListenerGroup is a set of related BOS endpoints: one plaintext, and
  69. // optionally one for SSL clients. Both listen in plaintext — a load balancer
  70. // terminates TLS and forwards decrypted traffic to the SSL endpoint. Pairing
  71. // them lets a redirect hand a client the sibling endpoint's advertised host,
  72. // so a session can upgrade to SSL or downgrade to plaintext on reconnect.
  73. type ListenerGroup struct {
  74. // Name is the URI scheme the group was parsed from, e.g. "LOCAL".
  75. Name string
  76. BOSListenAddress string
  77. BOSListenAddressSSL string
  78. BOSAdvertisedHostPlain string
  79. BOSAdvertisedHostSSL string
  80. KerberosListenAddress string
  81. }
  82. // HasSSL reports whether clients can reach this group over SSL. Config
  83. // validation guarantees such a group also has an SSL listen address.
  84. func (g ListenerGroup) HasSSL() bool {
  85. return g.BOSAdvertisedHostSSL != ""
  86. }
  87. // PlainEndpoint returns the group's plaintext BOS socket.
  88. func (g ListenerGroup) PlainEndpoint() Endpoint {
  89. return Endpoint{Group: g, ListenAddress: g.BOSListenAddress}
  90. }
  91. // SSLEndpoint returns the socket that receives decrypted traffic from the
  92. // group's SSL terminator. ok is false when SSL is not enabled for the group.
  93. func (g ListenerGroup) SSLEndpoint() (ep Endpoint, ok bool) {
  94. if !g.HasSSL() {
  95. return Endpoint{}, false
  96. }
  97. return Endpoint{Group: g, ListenAddress: g.BOSListenAddressSSL, IsSSL: true}, true
  98. }
  99. // Endpoints returns every BOS socket the group binds.
  100. func (g ListenerGroup) Endpoints() []Endpoint {
  101. eps := []Endpoint{g.PlainEndpoint()}
  102. if ssl, ok := g.SSLEndpoint(); ok {
  103. eps = append(eps, ssl)
  104. }
  105. return eps
  106. }
  107. // Endpoint is a single BOS socket. IsSSL means traffic arrives from an SSL
  108. // terminator, so clients that connect here stay on the SSL path.
  109. type Endpoint struct {
  110. Group ListenerGroup
  111. ListenAddress string
  112. IsSSL bool
  113. }
  114. // AdvertisedHost returns the BOS host clients on this endpoint reconnect to.
  115. func (e Endpoint) AdvertisedHost() string {
  116. if e.IsSSL {
  117. return e.Group.BOSAdvertisedHostSSL
  118. }
  119. return e.Group.BOSAdvertisedHostPlain
  120. }
  121. //go:generate go run ../cmd/config_generator unix settings.env basic
  122. //go:generate go run ../cmd/config_generator unix ssl/settings.env ssl
  123. type Config struct {
  124. 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"`
  125. 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"`
  126. 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"`
  127. 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."`
  128. 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"`
  129. 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"`
  130. 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)."`
  131. WebAPIEnabled bool `envconfig:"WEBAPI_ENABLED" required:"false" default:"true" basic:"true" ssl:"true" description:"Enable the WebAPI, which serves web-based AIM clients such as AIM Express. When enabled, the server listens on WEBAPI_LISTENERS."`
  132. WebAPIListeners []string `envconfig:"WEBAPI_LISTENERS" required:"false" basic:"0.0.0.0:8081" ssl:"0.0.0.0:8081" description:"Network listeners for WebAPI.\n\nFormat: Comma-separated list of hostname:port pairs.\n\nExamples:\n\t// All interfaces\n\t0.0.0.0:8081\n\t// Multiple listeners\n\t0.0.0.0:8081,192.168.1.10:8082"`
  133. WebAPIAllowedOrigins []string `envconfig:"WEBAPI_ALLOWED_ORIGINS" required:"false" basic:"http://localhost:*" ssl:"http://localhost:*" description:"Origins allowed to call the WebAPI from a browser (CORS). A browser blocks a cross-origin response whose origin is not listed here, so the client serving the web app must appear in this list.\n\nFormat:\n\t- Comma-separated list of [SCHEME]://[HOSTNAME]:[PORT]\n\t- An origin is the scheme, host and port together: a client served from another port needs its own entry\n\t- Omit the port when it is the scheme default, the way a browser writes it: https://aim.example.com, not https://aim.example.com:443\n\t- No trailing slash, path, query or fragment\n\t- An entry may contain one wildcard (*) standing in for 0 or more characters, in the host or the port: https://*.example.com, http://localhost:* . Only one wildcard per entry, the scheme cannot be wildcarded, and matching one costs a little more per request\n\t- A lone * allows any origin, which is also what an unset or empty value means\n\nExamples:\n\t// Single origin\n\thttps://ras.dev\n\t// Web app on a separate port, plus the API host itself\n\thttp://localhost:8000,https://ras.dev\n\t// Any origin (development only)\n\t*"`
  134. 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."`
  135. 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."`
  136. 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."`
  137. SystemMsgScreenName string `envconfig:"SYSTEM_MSG_SCREEN_NAME" required:"false" basic:"" ssl:"" description:"Screen name that server notifications, such as the multiple sign-in and offline message notices, are sent from. Defaults to 'OOS System Msg' when blank."`
  138. LogLevel string `envconfig:"LOG_LEVEL" required:"true" basic:"info" ssl:"info" description:"Set logging granularity. Possible values: 'trace', 'debug', 'info', 'warn', 'error'."`
  139. ICBMLegacyCharset string `envconfig:"ICBM_LEGACY_CHARSET" required:"false" default:"Windows1251" basic:"Windows1251" ssl:"Windows1251" description:"Fallback character set used to transcode IMs between Unicode-capable and legacy clients. Possible values: Windows1250 (central european: polish, czech, hungarian), Windows1251 (russian), Windows1252 (western european: english, french, german), Windows1253 (greek), Windows1254 (turkish), Windows1255 (hebrew), Windows1256 (arabic), Windows1257 (baltic: lithuanian, latvian, estonian), Windows1258 (vietnamese), KOI8R (russian), KOI8U (ukrainian)."`
  140. // ICQ Legacy Protocol Configuration
  141. ICQLegacy ICQLegacyConfig
  142. }
  143. // ICQLegacyConfig holds configuration for legacy ICQ protocol support (v2-v5)
  144. type ICQLegacyConfig struct {
  145. 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."`
  146. 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"`
  147. 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)."`
  148. 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."`
  149. 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."`
  150. 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."`
  151. DepartmentsEnabled bool `envconfig:"ICQ_LEGACY_DEPARTMENTS_ENABLED" required:"false" basic:"false" ssl:"false" description:"Enable department listing feature (groupware functionality)."`
  152. BroadcastEnabled bool `envconfig:"ICQ_LEGACY_BROADCAST_ENABLED" required:"false" basic:"true" ssl:"true" description:"Enable broadcast message functionality."`
  153. WWPEnabled bool `envconfig:"ICQ_LEGACY_WWP_ENABLED" required:"false" basic:"true" ssl:"true" description:"Enable Web Pager (WWP) message support."`
  154. 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"`
  155. }
  156. // DefaultICQLegacyConfig returns the default configuration for ICQ legacy protocol
  157. func DefaultICQLegacyConfig() ICQLegacyConfig {
  158. return ICQLegacyConfig{
  159. Enabled: true,
  160. UDPListener: "0.0.0.0:4000",
  161. SupportedVersions: []int{2, 3, 4, 5},
  162. SessionTimeout: 120 * time.Second,
  163. KeepAliveInterval: 120 * time.Second,
  164. AutoRegistration: false,
  165. DepartmentsEnabled: false,
  166. BroadcastEnabled: true,
  167. WWPEnabled: true,
  168. }
  169. }
  170. // SupportsVersion checks if a specific protocol version is enabled
  171. func (c *ICQLegacyConfig) SupportsVersion(version int) bool {
  172. for _, v := range c.SupportedVersions {
  173. if v == version {
  174. return true
  175. }
  176. }
  177. return false
  178. }
  179. // DirectConnectionEnabled checks if direct connections are enabled for a specific protocol version
  180. func (c *ICQLegacyConfig) DirectConnectionEnabled(version int) bool {
  181. for _, v := range c.DirectConnections {
  182. if v == version {
  183. return true
  184. }
  185. }
  186. return false
  187. }
  188. func (c *Config) ParseListenersCfg() ([]ListenerGroup, error) {
  189. // Helper function to parse and validate a single URI
  190. parseURI := func(uriStr string) (*url.URL, error) {
  191. uriStr = strings.TrimSpace(uriStr)
  192. if uriStr == "" {
  193. return nil, nil
  194. }
  195. u, err := url.Parse(uriStr)
  196. if err != nil {
  197. return nil, uriFormatError{URI: uriStr, Err: err}
  198. }
  199. switch {
  200. case u.Scheme == "":
  201. return nil, uriFormatError{URI: uriStr, Err: errors.New("missing scheme")}
  202. case u.Hostname() == "":
  203. return nil, uriFormatError{URI: uriStr, Err: errors.New("missing host")}
  204. case u.Port() == "":
  205. return nil, uriFormatError{URI: uriStr, Err: errors.New("missing port")}
  206. }
  207. return u, nil
  208. }
  209. m := make(map[string]*ListenerGroup)
  210. // Parse BOS listeners
  211. for _, uriStr := range c.BOSListeners {
  212. u, err := parseURI(uriStr)
  213. if err != nil {
  214. return nil, err
  215. }
  216. if u == nil {
  217. continue
  218. }
  219. if _, ok := m[u.Scheme]; !ok {
  220. m[u.Scheme] = &ListenerGroup{}
  221. }
  222. if m[u.Scheme].BOSListenAddress != "" {
  223. return nil, errDuplicateListener
  224. }
  225. m[u.Scheme].BOSListenAddress = net.JoinHostPort(u.Hostname(), u.Port())
  226. }
  227. // Parse SSL BOS listeners
  228. for _, uriStr := range c.BOSListenersSSL {
  229. u, err := parseURI(uriStr)
  230. if err != nil {
  231. return nil, err
  232. }
  233. if u == nil {
  234. continue
  235. }
  236. if _, ok := m[u.Scheme]; !ok {
  237. m[u.Scheme] = &ListenerGroup{}
  238. }
  239. if m[u.Scheme].BOSListenAddressSSL != "" {
  240. return nil, errDuplicateListener
  241. }
  242. m[u.Scheme].BOSListenAddressSSL = net.JoinHostPort(u.Hostname(), u.Port())
  243. }
  244. // Parse plaintext BOS advertised listeners
  245. for _, uriStr := range c.BOSAdvertisedHostsPlain {
  246. u, err := parseURI(uriStr)
  247. if err != nil {
  248. return nil, err
  249. }
  250. if u == nil {
  251. continue
  252. }
  253. if _, ok := m[u.Scheme]; !ok {
  254. m[u.Scheme] = &ListenerGroup{}
  255. }
  256. if m[u.Scheme].BOSAdvertisedHostPlain != "" {
  257. return nil, errDuplicateListener
  258. }
  259. m[u.Scheme].BOSAdvertisedHostPlain = net.JoinHostPort(u.Hostname(), u.Port())
  260. }
  261. // Parse SSL BOS advertised listeners
  262. for _, uriStr := range c.BOSAdvertisedHostsSSL {
  263. u, err := parseURI(uriStr)
  264. if err != nil {
  265. return nil, err
  266. }
  267. if u == nil {
  268. continue
  269. }
  270. if _, ok := m[u.Scheme]; !ok {
  271. m[u.Scheme] = &ListenerGroup{}
  272. }
  273. if m[u.Scheme].BOSAdvertisedHostSSL != "" {
  274. return nil, errDuplicateListener
  275. }
  276. m[u.Scheme].BOSAdvertisedHostSSL = net.JoinHostPort(u.Hostname(), u.Port())
  277. }
  278. // Parse Kerberos listeners
  279. for _, uriStr := range c.KerberosListeners {
  280. u, err := parseURI(uriStr)
  281. if err != nil {
  282. return nil, err
  283. }
  284. if u == nil {
  285. continue
  286. }
  287. if _, ok := m[u.Scheme]; !ok {
  288. m[u.Scheme] = &ListenerGroup{}
  289. }
  290. if m[u.Scheme].KerberosListenAddress != "" {
  291. return nil, errDuplicateListener
  292. }
  293. m[u.Scheme].KerberosListenAddress = net.JoinHostPort(u.Hostname(), u.Port())
  294. }
  295. ret := make([]ListenerGroup, 0, len(m))
  296. for k, v := range m {
  297. switch {
  298. case v.BOSAdvertisedHostPlain == "":
  299. return nil, fmt.Errorf("missing BOS advertise address for listener `%s://`", k)
  300. case v.BOSListenAddress == "":
  301. return nil, fmt.Errorf("missing BOS listen address for listener `%s://`", k)
  302. case v.HasSSL() && v.BOSListenAddressSSL == "":
  303. return nil, fmt.Errorf("missing SSL BOS listen address for listener `%s://`. OSCAR_ADVERTISED_LISTENERS_SSL defines this listener, so OSCAR_LISTENERS_SSL needs a matching entry for the SSL terminator (e.g. OSCAR_LISTENERS_SSL=%s://0.0.0.0:5191)", k, strings.ToUpper(k))
  304. }
  305. v.Name = k
  306. ret = append(ret, *v)
  307. }
  308. if len(ret) == 0 {
  309. return nil, errNoBOSListeners
  310. }
  311. // Catch sockets that collide across lists or groups, which would otherwise
  312. // surface at bind time as a bare "address already in use".
  313. seen := make(map[string]string, len(ret)*3)
  314. for _, l := range ret {
  315. for _, socket := range []struct{ envVar, addr string }{
  316. {"OSCAR_LISTENERS", l.BOSListenAddress},
  317. {"OSCAR_LISTENERS_SSL", l.BOSListenAddressSSL},
  318. {"KERBEROS_LISTENERS", l.KerberosListenAddress},
  319. } {
  320. if socket.addr == "" {
  321. continue
  322. }
  323. src := fmt.Sprintf("%s `%s://`", socket.envVar, l.Name)
  324. if prev, ok := seen[socket.addr]; ok {
  325. return nil, fmt.Errorf("listen address %s is configured for both %s and %s", socket.addr, prev, src)
  326. }
  327. seen[socket.addr] = src
  328. }
  329. }
  330. return ret, nil
  331. }
  332. // legacyCharsetNames lists the supported ICBM_LEGACY_CHARSET values in the
  333. // order they are reported to the user.
  334. var legacyCharsetNames = []string{
  335. "Windows1250", "Windows1251", "Windows1252", "Windows1253", "Windows1254",
  336. "Windows1255", "Windows1256", "Windows1257", "Windows1258", "KOI8R", "KOI8U",
  337. }
  338. // legacyCharsets maps lower-cased ICBM_LEGACY_CHARSET values to charmaps.
  339. var legacyCharsets = map[string]*charmap.Charmap{
  340. "windows1250": charmap.Windows1250,
  341. "windows1251": charmap.Windows1251,
  342. "windows1252": charmap.Windows1252,
  343. "windows1253": charmap.Windows1253,
  344. "windows1254": charmap.Windows1254,
  345. "windows1255": charmap.Windows1255,
  346. "windows1256": charmap.Windows1256,
  347. "windows1257": charmap.Windows1257,
  348. "windows1258": charmap.Windows1258,
  349. "koi8r": charmap.KOI8R,
  350. "koi8u": charmap.KOI8U,
  351. }
  352. // ParseICBMLegacyCharset returns the encoding named by ICBM_LEGACY_CHARSET,
  353. // matched case-insensitively.
  354. func (c *Config) ParseICBMLegacyCharset() (encoding.Encoding, error) {
  355. cm, ok := legacyCharsets[strings.ToLower(strings.TrimSpace(c.ICBMLegacyCharset))]
  356. if !ok {
  357. return nil, fmt.Errorf("unsupported ICBM_LEGACY_CHARSET %q. Possible values: %s", c.ICBMLegacyCharset, strings.Join(legacyCharsetNames, ", "))
  358. }
  359. return cm, nil
  360. }
  361. func (c *Config) Validate() error {
  362. // Validate TOCListeners (format: hostname:port pairs). Every entry is
  363. // bound as given, and an empty address binds a random port on all
  364. // interfaces.
  365. for _, listener := range c.TOCListeners {
  366. listener = strings.TrimSpace(listener)
  367. if listener == "" {
  368. return fmt.Errorf("invalid TOC listeners %q: empty entry. Remove the trailing or doubled comma from TOC_LISTENERS", strings.Join(c.TOCListeners, ","))
  369. }
  370. host, port, err := net.SplitHostPort(listener)
  371. if err != nil {
  372. return fmt.Errorf("invalid TOC listener %q: %v. Valid format: HOST:PORT (e.g., 0.0.0.0:9898)", listener, err)
  373. }
  374. if host == "" {
  375. return fmt.Errorf("invalid TOC listener %q: missing host. Valid format: HOST:PORT (e.g., 0.0.0.0:9898)", listener)
  376. }
  377. if port == "" {
  378. return fmt.Errorf("invalid TOC listener %q: missing port. Valid format: HOST:PORT (e.g., 0.0.0.0:9898)", listener)
  379. }
  380. }
  381. // Validate APIListener (format: hostname:port pair, no scheme)
  382. apiListener := strings.TrimSpace(c.APIListener)
  383. if apiListener == "" {
  384. return fmt.Errorf("APIListener is required and cannot be empty")
  385. }
  386. host, port, err := net.SplitHostPort(apiListener)
  387. if err != nil {
  388. return fmt.Errorf("invalid API listener %q: %v. Valid format: HOST:PORT (e.g., 127.0.0.1:8080)", c.APIListener, err)
  389. }
  390. if host == "" {
  391. return fmt.Errorf("invalid API listener %q: missing host. Valid format: HOST:PORT (e.g., 127.0.0.1:8080)", c.APIListener)
  392. }
  393. if port == "" {
  394. return fmt.Errorf("invalid API listener %q: missing port. Valid format: HOST:PORT (e.g., 127.0.0.1:8080)", c.APIListener)
  395. }
  396. // Validate WebAPIListeners (format: hostname:port pairs, no scheme). Every
  397. // entry is bound as given, and an empty address binds :80 on all interfaces.
  398. for _, listener := range c.WebAPIListeners {
  399. listener = strings.TrimSpace(listener)
  400. if listener == "" {
  401. return fmt.Errorf("invalid web API listeners %q: empty entry. Remove the trailing or doubled comma from WEBAPI_LISTENERS", strings.Join(c.WebAPIListeners, ","))
  402. }
  403. host, port, err := net.SplitHostPort(listener)
  404. if err != nil {
  405. return fmt.Errorf("invalid web API listener %q: %v. Valid format: HOST:PORT (e.g., 0.0.0.0:8081)", listener, err)
  406. }
  407. if host == "" {
  408. return fmt.Errorf("invalid web API listener %q: missing host. Valid format: HOST:PORT (e.g., 0.0.0.0:8081)", listener)
  409. }
  410. if port == "" {
  411. return fmt.Errorf("invalid web API listener %q: missing port. Valid format: HOST:PORT (e.g., 0.0.0.0:8081)", listener)
  412. }
  413. }
  414. // Validate WebAPIAllowedOrigins (format: scheme://host[:port], or * for any).
  415. // An empty list is valid and means any origin is allowed.
  416. for _, origin := range c.WebAPIAllowedOrigins {
  417. origin = strings.TrimSpace(origin)
  418. if origin == "" || origin == "*" {
  419. continue
  420. }
  421. if err := validateAllowedOrigin(origin); err != nil {
  422. return err
  423. }
  424. }
  425. return nil
  426. }