package main import ( "errors" "fmt" "log" "net/http" "net/url" "os" "strconv" "strings" "time" "github.com/gin-gonic/gin" "gzzn.com/mini/ciimsproxy/internal" ) const ( servicePrefix = "/services/ExchangeService" defaultTimeout = 240 maxMsgLen = 1048576 // 1MB maxCount = 1000 readHeaderTimeout = 5 * time.Second readTimeout = 10 * time.Second writeTimeoutGrace = 10 * time.Second idleTimeout = 60 * time.Second allowedServersEnvVarName = "CIIMS_ALLOWED_SERVERS" ) type Config struct { ServerURL string AllowedServers map[string]string Listen string Timeout int Client *internal.Client } type App struct { config Config } type sendRequest struct { URL string `json:"url"` User string `json:"user" binding:"required"` Pass string `json:"pass" binding:"required"` Event string `json:"event" binding:"required"` Priority int `json:"priority"` Val bool `json:"val"` Msg string `json:"msg" binding:"required"` } type receiveRequest struct { URL string `json:"url"` User string `json:"user" binding:"required"` Pass string `json:"pass" binding:"required"` Count int `json:"count" binding:"required"` } func main() { config, err := loadConfig() if err != nil { log.Printf("[ERROR] configuration error: %v", err) os.Exit(1) } log.Printf("use ciims: %s", config.ServerURL) srv := newServer(config) log.Printf("Starting ciims proxy for %s", config.ServerURL) log.Printf("timeout: %d", config.Timeout) if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Printf("[ERROR] server failed: %v", err) os.Exit(1) } } func loadConfig() (Config, error) { config := Config{ Listen: os.Getenv("PROXY_LISTEN"), Timeout: defaultTimeout, } serverURL, err := normalizeBaseURL(os.Getenv("CIIMS_SERVER")) if err != nil { return Config{}, fmt.Errorf("invalid CIIMS_SERVER: %w", err) } config.ServerURL = serverURL config.AllowedServers = map[string]string{serverURL: serverURL} allowedServers, err := parseAllowedServers(os.Getenv(allowedServersEnvVarName)) if err != nil { return Config{}, err } for _, allowed := range allowedServers { config.AllowedServers[allowed] = allowed } t, err := getIntEnv("CIIMS_TIMEOUT") if err != nil { return Config{}, err } if t > 0 { config.Timeout = t } if config.Listen == "" { config.Listen = ":9090" } config.Client, err = internal.NewClient(config.Timeout) if err != nil { return Config{}, err } return config, nil } func newServer(config Config) *http.Server { return &http.Server{ Addr: config.Listen, Handler: newRouter(config), ReadHeaderTimeout: readHeaderTimeout, ReadTimeout: readTimeout, WriteTimeout: time.Duration(config.Timeout)*time.Second + writeTimeoutGrace, IdleTimeout: idleTimeout, } } func newRouter(config Config) *gin.Engine { if config.AllowedServers == nil && config.ServerURL != "" { config.AllowedServers = map[string]string{config.ServerURL: config.ServerURL} } app := &App{config: config} r := gin.Default() r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{ "message": "pong", }) }) r.POST("/send", app.sendMessage) r.POST("/receive", app.receiveMessage) return r } func (a *App) sendMessage(c *gin.Context) { c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxMsgLen) var message sendRequest if err := c.ShouldBind(&message); err != nil { a.handleBindError(c, err) return } ciimsURL, ok := a.resolveTargetURL(c, message.URL) if !ok { return } msg := internal.CreateSend(message.User, message.Pass, message.Priority, message.Event, message.Val, message.Msg) resp, err := a.config.Client.Send(c.Request.Context(), ciimsURL, msg) if err != nil { a.handleSendError(c, "send", resp, err) return } if errMsg := resp.ErrorMessage(); errMsg != "" { log.Printf("[ERROR] SOAP fault: %s", errMsg) c.JSON(http.StatusInternalServerError, gin.H{"error": errMsg}) return } c.JSON(http.StatusOK, gin.H{"error": ""}) } func (a *App) receiveMessage(c *gin.Context) { c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxMsgLen) var message receiveRequest if err := c.ShouldBind(&message); err != nil { a.handleBindError(c, err) return } if message.Count < 1 || message.Count > maxCount { c.JSON(http.StatusBadRequest, gin.H{"error": "count must be between 1 and " + strconv.Itoa(maxCount)}) return } ciimsURL, ok := a.resolveTargetURL(c, message.URL) if !ok { return } msg := internal.CreateReceive(message.User, message.Pass, message.Count) resp, err := a.config.Client.Send(c.Request.Context(), ciimsURL, msg) if err != nil { a.handleSendError(c, "receive", resp, err) return } if errMsg := resp.ErrorMessage(); errMsg != "" { log.Printf("[ERROR] SOAP fault: %s", errMsg) c.JSON(http.StatusInternalServerError, gin.H{"error": errMsg}) return } c.JSON(http.StatusOK, gin.H{"msgs": resp.Messages()}) } func (a *App) resolveTargetURL(c *gin.Context, requestedURL string) (string, bool) { targetBase, err := a.getBaseURL(requestedURL) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return "", false } return targetBase + servicePrefix, true } func (a *App) getBaseURL(requestedURL string) (string, error) { if strings.TrimSpace(requestedURL) == "" { return a.config.ServerURL, nil } normalized, err := normalizeBaseURL(requestedURL) if err != nil { return "", fmt.Errorf("invalid url: %w", err) } if _, ok := a.config.AllowedServers[normalized]; !ok { return "", fmt.Errorf("url is not allowed") } return normalized, nil } func normalizeBaseURL(raw string) (string, error) { raw = strings.TrimSpace(raw) if raw == "" { return "", fmt.Errorf("must not be empty") } u, err := url.Parse(raw) if err != nil { return "", err } u.Scheme = strings.ToLower(u.Scheme) u.Host = strings.ToLower(u.Host) if u.Scheme != "http" && u.Scheme != "https" { return "", fmt.Errorf("scheme must be http or https") } if u.Host == "" { return "", fmt.Errorf("host is required") } if u.RawQuery != "" || u.Fragment != "" { return "", fmt.Errorf("query and fragment are not allowed") } u.Path = strings.TrimRight(u.Path, "/") u.RawPath = "" return u.String(), nil } func parseAllowedServers(raw string) ([]string, error) { if strings.TrimSpace(raw) == "" { return nil, nil } parts := strings.Split(raw, ",") allowed := make([]string, 0, len(parts)) for _, part := range parts { normalized, err := normalizeBaseURL(part) if err != nil { return nil, fmt.Errorf("invalid %s entry %q: %w", allowedServersEnvVarName, part, err) } allowed = append(allowed, normalized) } return allowed, nil } func (a *App) handleBindError(c *gin.Context, err error) { var maxBytesErr *http.MaxBytesError if errors.As(err, &maxBytesErr) { c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": err.Error()}) return } log.Print(err.Error()) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) } func (a *App) handleSendError(c *gin.Context, operation string, resp *internal.CIIMSResponse, err error) { if resp != nil { if errMsg := resp.ErrorMessage(); errMsg != "" { log.Printf("[ERROR] SOAP fault: %s", errMsg) c.JSON(http.StatusInternalServerError, gin.H{"error": errMsg}) return } } var statusErr *internal.HTTPStatusError if errors.As(err, &statusErr) { if errMsg := internal.GetErrMsg(statusErr.Body); errMsg != "" { log.Printf("[ERROR] SOAP fault: %s", errMsg) c.JSON(http.StatusInternalServerError, gin.H{"error": errMsg}) return } } log.Printf("[ERROR] %s failed: %v", operation, err) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } func getIntEnv(key string) (int, error) { val := os.Getenv(key) if val == "" { return 0, nil } ret, err := strconv.Atoi(val) if err != nil { return 0, fmt.Errorf("invalid %s value %q: must be an integer", key, val) } if ret <= 0 { return 0, fmt.Errorf("invalid %s value %q: must be positive", key, val) } return ret, nil }