config.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  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. 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"`
  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. LogLevel string `envconfig:"LOG_LEVEL" required:"true" basic:"info" ssl:"info" description:"Set logging granularity. Possible values: 'trace', 'debug', 'info', 'warn', 'error'."`
  138. 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)."`
  139. // ICQ Legacy Protocol Configuration
  140. ICQLegacy ICQLegacyConfig
  141. }
  142. // ICQLegacyConfig holds configuration for legacy ICQ protocol support (v2-v5)
  143. type ICQLegacyConfig struct {
  144. 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."`
  145. 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"`
  146. 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)."`
  147. 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."`
  148. 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."`
  149. 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."`
  150. DepartmentsEnabled bool `envconfig:"ICQ_LEGACY_DEPARTMENTS_ENABLED" required:"false" basic:"false" ssl:"false" description:"Enable department listing feature (groupware functionality)."`
  151. BroadcastEnabled bool `envconfig:"ICQ_LEGACY_BROADCAST_ENABLED" required:"false" basic:"true" ssl:"true" description:"Enable broadcast message functionality."`
  152. WWPEnabled bool `envconfig:"ICQ_LEGACY_WWP_ENABLED" required:"false" basic:"true" ssl:"true" description:"Enable Web Pager (WWP) message support."`
  153. 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"`
  154. }
  155. // DefaultICQLegacyConfig returns the default configuration for ICQ legacy protocol
  156. func DefaultICQLegacyConfig() ICQLegacyConfig {
  157. return ICQLegacyConfig{
  158. Enabled: true,
  159. UDPListener: "0.0.0.0:4000",
  160. SupportedVersions: []int{2, 3, 4, 5},
  161. SessionTimeout: 120 * time.Second,
  162. KeepAliveInterval: 120 * time.Second,
  163. AutoRegistration: false,
  164. DepartmentsEnabled: false,
  165. BroadcastEnabled: true,
  166. WWPEnabled: true,
  167. }
  168. }
  169. // SupportsVersion checks if a specific protocol version is enabled
  170. func (c *ICQLegacyConfig) SupportsVersion(version int) bool {
  171. for _, v := range c.SupportedVersions {
  172. if v == version {
  173. return true
  174. }
  175. }
  176. return false
  177. }
  178. // DirectConnectionEnabled checks if direct connections are enabled for a specific protocol version
  179. func (c *ICQLegacyConfig) DirectConnectionEnabled(version int) bool {
  180. for _, v := range c.DirectConnections {
  181. if v == version {
  182. return true
  183. }
  184. }
  185. return false
  186. }
  187. func (c *Config) ParseListenersCfg() ([]ListenerGroup, error) {
  188. // Helper function to parse and validate a single URI
  189. parseURI := func(uriStr string) (*url.URL, error) {
  190. uriStr = strings.TrimSpace(uriStr)
  191. if uriStr == "" {
  192. return nil, nil
  193. }
  194. u, err := url.Parse(uriStr)
  195. if err != nil {
  196. return nil, uriFormatError{URI: uriStr, Err: err}
  197. }
  198. switch {
  199. case u.Scheme == "":
  200. return nil, uriFormatError{URI: uriStr, Err: errors.New("missing scheme")}
  201. case u.Hostname() == "":
  202. return nil, uriFormatError{URI: uriStr, Err: errors.New("missing host")}
  203. case u.Port() == "":
  204. return nil, uriFormatError{URI: uriStr, Err: errors.New("missing port")}
  205. }
  206. return u, nil
  207. }
  208. m := make(map[string]*ListenerGroup)
  209. // Parse BOS listeners
  210. for _, uriStr := range c.BOSListeners {
  211. u, err := parseURI(uriStr)
  212. if err != nil {
  213. return nil, err
  214. }
  215. if u == nil {
  216. continue
  217. }
  218. if _, ok := m[u.Scheme]; !ok {
  219. m[u.Scheme] = &ListenerGroup{}
  220. }
  221. if m[u.Scheme].BOSListenAddress != "" {
  222. return nil, errDuplicateListener
  223. }
  224. m[u.Scheme].BOSListenAddress = net.JoinHostPort(u.Hostname(), u.Port())
  225. }
  226. // Parse SSL BOS listeners
  227. for _, uriStr := range c.BOSListenersSSL {
  228. u, err := parseURI(uriStr)
  229. if err != nil {
  230. return nil, err
  231. }
  232. if u == nil {
  233. continue
  234. }
  235. if _, ok := m[u.Scheme]; !ok {
  236. m[u.Scheme] = &ListenerGroup{}
  237. }
  238. if m[u.Scheme].BOSListenAddressSSL != "" {
  239. return nil, errDuplicateListener
  240. }
  241. m[u.Scheme].BOSListenAddressSSL = net.JoinHostPort(u.Hostname(), u.Port())
  242. }
  243. // Parse plaintext BOS advertised listeners
  244. for _, uriStr := range c.BOSAdvertisedHostsPlain {
  245. u, err := parseURI(uriStr)
  246. if err != nil {
  247. return nil, err
  248. }
  249. if u == nil {
  250. continue
  251. }
  252. if _, ok := m[u.Scheme]; !ok {
  253. m[u.Scheme] = &ListenerGroup{}
  254. }
  255. if m[u.Scheme].BOSAdvertisedHostPlain != "" {
  256. return nil, errDuplicateListener
  257. }
  258. m[u.Scheme].BOSAdvertisedHostPlain = net.JoinHostPort(u.Hostname(), u.Port())
  259. }
  260. // Parse SSL BOS advertised listeners
  261. for _, uriStr := range c.BOSAdvertisedHostsSSL {
  262. u, err := parseURI(uriStr)
  263. if err != nil {
  264. return nil, err
  265. }
  266. if u == nil {
  267. continue
  268. }
  269. if _, ok := m[u.Scheme]; !ok {
  270. m[u.Scheme] = &ListenerGroup{}
  271. }
  272. if m[u.Scheme].BOSAdvertisedHostSSL != "" {
  273. return nil, errDuplicateListener
  274. }
  275. m[u.Scheme].BOSAdvertisedHostSSL = net.JoinHostPort(u.Hostname(), u.Port())
  276. }
  277. // Parse Kerberos listeners
  278. for _, uriStr := range c.KerberosListeners {
  279. u, err := parseURI(uriStr)
  280. if err != nil {
  281. return nil, err
  282. }
  283. if u == nil {
  284. continue
  285. }
  286. if _, ok := m[u.Scheme]; !ok {
  287. m[u.Scheme] = &ListenerGroup{}
  288. }
  289. if m[u.Scheme].KerberosListenAddress != "" {
  290. return nil, errDuplicateListener
  291. }
  292. m[u.Scheme].KerberosListenAddress = net.JoinHostPort(u.Hostname(), u.Port())
  293. }
  294. ret := make([]ListenerGroup, 0, len(m))
  295. for k, v := range m {
  296. switch {
  297. case v.BOSAdvertisedHostPlain == "":
  298. return nil, fmt.Errorf("missing BOS advertise address for listener `%s://`", k)
  299. case v.BOSListenAddress == "":
  300. return nil, fmt.Errorf("missing BOS listen address for listener `%s://`", k)
  301. case v.HasSSL() && v.BOSListenAddressSSL == "":
  302. 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))
  303. }
  304. v.Name = k
  305. ret = append(ret, *v)
  306. }
  307. if len(ret) == 0 {
  308. return nil, errNoBOSListeners
  309. }
  310. // Catch sockets that collide across lists or groups, which would otherwise
  311. // surface at bind time as a bare "address already in use".
  312. seen := make(map[string]string, len(ret)*3)
  313. for _, l := range ret {
  314. for _, socket := range []struct{ envVar, addr string }{
  315. {"OSCAR_LISTENERS", l.BOSListenAddress},
  316. {"OSCAR_LISTENERS_SSL", l.BOSListenAddressSSL},
  317. {"KERBEROS_LISTENERS", l.KerberosListenAddress},
  318. } {
  319. if socket.addr == "" {
  320. continue
  321. }
  322. src := fmt.Sprintf("%s `%s://`", socket.envVar, l.Name)
  323. if prev, ok := seen[socket.addr]; ok {
  324. return nil, fmt.Errorf("listen address %s is configured for both %s and %s", socket.addr, prev, src)
  325. }
  326. seen[socket.addr] = src
  327. }
  328. }
  329. return ret, nil
  330. }
  331. // legacyCharsetNames lists the supported ICBM_LEGACY_CHARSET values in the
  332. // order they are reported to the user.
  333. var legacyCharsetNames = []string{
  334. "Windows1250", "Windows1251", "Windows1252", "Windows1253", "Windows1254",
  335. "Windows1255", "Windows1256", "Windows1257", "Windows1258", "KOI8R", "KOI8U",
  336. }
  337. // legacyCharsets maps lower-cased ICBM_LEGACY_CHARSET values to charmaps.
  338. var legacyCharsets = map[string]*charmap.Charmap{
  339. "windows1250": charmap.Windows1250,
  340. "windows1251": charmap.Windows1251,
  341. "windows1252": charmap.Windows1252,
  342. "windows1253": charmap.Windows1253,
  343. "windows1254": charmap.Windows1254,
  344. "windows1255": charmap.Windows1255,
  345. "windows1256": charmap.Windows1256,
  346. "windows1257": charmap.Windows1257,
  347. "windows1258": charmap.Windows1258,
  348. "koi8r": charmap.KOI8R,
  349. "koi8u": charmap.KOI8U,
  350. }
  351. // ParseICBMLegacyCharset returns the encoding named by ICBM_LEGACY_CHARSET,
  352. // matched case-insensitively.
  353. func (c *Config) ParseICBMLegacyCharset() (encoding.Encoding, error) {
  354. cm, ok := legacyCharsets[strings.ToLower(strings.TrimSpace(c.ICBMLegacyCharset))]
  355. if !ok {
  356. return nil, fmt.Errorf("unsupported ICBM_LEGACY_CHARSET %q. Possible values: %s", c.ICBMLegacyCharset, strings.Join(legacyCharsetNames, ", "))
  357. }
  358. return cm, nil
  359. }
  360. func (c *Config) Validate() error {
  361. // Validate TOCListeners (format: hostname:port pairs)
  362. for _, listener := range c.TOCListeners {
  363. listener = strings.TrimSpace(listener)
  364. if listener == "" {
  365. continue
  366. }
  367. host, port, err := net.SplitHostPort(listener)
  368. if err != nil {
  369. return fmt.Errorf("invalid TOC listener %q: %v. Valid format: HOST:PORT (e.g., 0.0.0.0:9898)", listener, err)
  370. }
  371. if host == "" {
  372. return fmt.Errorf("invalid TOC listener %q: missing host. Valid format: HOST:PORT (e.g., 0.0.0.0:9898)", listener)
  373. }
  374. if port == "" {
  375. return fmt.Errorf("invalid TOC listener %q: missing port. Valid format: HOST:PORT (e.g., 0.0.0.0:9898)", listener)
  376. }
  377. }
  378. // Validate APIListener (format: hostname:port pair, no scheme)
  379. apiListener := strings.TrimSpace(c.APIListener)
  380. if apiListener == "" {
  381. return fmt.Errorf("APIListener is required and cannot be empty")
  382. }
  383. host, port, err := net.SplitHostPort(apiListener)
  384. if err != nil {
  385. return fmt.Errorf("invalid API listener %q: %v. Valid format: HOST:PORT (e.g., 127.0.0.1:8080)", c.APIListener, err)
  386. }
  387. if host == "" {
  388. return fmt.Errorf("invalid API listener %q: missing host. Valid format: HOST:PORT (e.g., 127.0.0.1:8080)", c.APIListener)
  389. }
  390. if port == "" {
  391. return fmt.Errorf("invalid API listener %q: missing port. Valid format: HOST:PORT (e.g., 127.0.0.1:8080)", c.APIListener)
  392. }
  393. // Validate WebAPIListeners (format: hostname:port pairs, no scheme)
  394. for _, listener := range c.WebAPIListeners {
  395. listener = strings.TrimSpace(listener)
  396. if listener == "" {
  397. continue
  398. }
  399. host, port, err := net.SplitHostPort(listener)
  400. if err != nil {
  401. return fmt.Errorf("invalid web API listener %q: %v. Valid format: HOST:PORT (e.g., 0.0.0.0:8081)", listener, err)
  402. }
  403. if host == "" {
  404. return fmt.Errorf("invalid web API listener %q: missing host. Valid format: HOST:PORT (e.g., 0.0.0.0:8081)", listener)
  405. }
  406. if port == "" {
  407. return fmt.Errorf("invalid web API listener %q: missing port. Valid format: HOST:PORT (e.g., 0.0.0.0:8081)", listener)
  408. }
  409. }
  410. // Validate WebAPIAllowedOrigins (format: scheme://host[:port], or * for any).
  411. // An empty list is valid and means any origin is allowed.
  412. for _, origin := range c.WebAPIAllowedOrigins {
  413. origin = strings.TrimSpace(origin)
  414. if origin == "" || origin == "*" {
  415. continue
  416. }
  417. if err := validateAllowedOrigin(origin); err != nil {
  418. return err
  419. }
  420. }
  421. return nil
  422. }