2020-05-11 14:58:22 +08:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
2026-07-08 15:17:07 +08:00
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"log"
|
2020-05-11 14:58:22 +08:00
|
|
|
"net/http"
|
2026-07-08 16:23:00 +08:00
|
|
|
"net/url"
|
2020-09-15 10:24:08 +08:00
|
|
|
"os"
|
2026-07-08 15:17:07 +08:00
|
|
|
"strconv"
|
2026-07-08 16:23:00 +08:00
|
|
|
"strings"
|
|
|
|
|
"time"
|
2020-05-11 14:58:22 +08:00
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
"gzzn.com/mini/ciimsproxy/internal"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
2026-07-08 16:23:00 +08:00
|
|
|
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"
|
2020-05-11 14:58:22 +08:00
|
|
|
)
|
|
|
|
|
|
2026-07-08 15:17:07 +08:00
|
|
|
type Config struct {
|
2026-07-08 16:23:00 +08:00
|
|
|
ServerURL string
|
|
|
|
|
AllowedServers map[string]string
|
|
|
|
|
Listen string
|
|
|
|
|
Timeout int
|
|
|
|
|
Client *internal.Client
|
2026-07-08 15:17:07 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type App struct {
|
|
|
|
|
config Config
|
|
|
|
|
}
|
2020-09-15 10:24:08 +08:00
|
|
|
|
2026-07-08 16:23:00 +08:00
|
|
|
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"`
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-11 14:58:22 +08:00
|
|
|
func main() {
|
2026-07-08 15:17:07 +08:00
|
|
|
config, err := loadConfig()
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("[ERROR] configuration error: %v", err)
|
|
|
|
|
os.Exit(1)
|
2020-09-15 10:24:08 +08:00
|
|
|
}
|
2026-07-08 15:17:07 +08:00
|
|
|
|
|
|
|
|
log.Printf("use ciims: %s", config.ServerURL)
|
2026-07-08 16:23:00 +08:00
|
|
|
srv := newServer(config)
|
2026-07-08 15:17:07 +08:00
|
|
|
log.Printf("Starting ciims proxy for %s", config.ServerURL)
|
|
|
|
|
log.Printf("timeout: %d", config.Timeout)
|
2026-07-08 16:23:00 +08:00
|
|
|
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
2026-07-08 15:17:07 +08:00
|
|
|
log.Printf("[ERROR] server failed: %v", err)
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func loadConfig() (Config, error) {
|
|
|
|
|
config := Config{
|
2026-07-08 16:23:00 +08:00
|
|
|
Listen: os.Getenv("PROXY_LISTEN"),
|
|
|
|
|
Timeout: defaultTimeout,
|
2026-07-08 15:17:07 +08:00
|
|
|
}
|
2026-07-08 16:23:00 +08:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 15:17:07 +08:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 16:23:00 +08:00
|
|
|
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,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 15:17:07 +08:00
|
|
|
func newRouter(config Config) *gin.Engine {
|
2026-07-08 16:23:00 +08:00
|
|
|
if config.AllowedServers == nil && config.ServerURL != "" {
|
|
|
|
|
config.AllowedServers = map[string]string{config.ServerURL: config.ServerURL}
|
|
|
|
|
}
|
2026-07-08 15:17:07 +08:00
|
|
|
app := &App{config: config}
|
2020-05-11 14:58:22 +08:00
|
|
|
r := gin.Default()
|
|
|
|
|
r.GET("/ping", func(c *gin.Context) {
|
|
|
|
|
c.JSON(200, gin.H{
|
|
|
|
|
"message": "pong",
|
|
|
|
|
})
|
|
|
|
|
})
|
2026-07-08 15:17:07 +08:00
|
|
|
r.POST("/send", app.sendMessage)
|
|
|
|
|
r.POST("/receive", app.receiveMessage)
|
|
|
|
|
return r
|
2020-05-11 14:58:22 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-08 15:17:07 +08:00
|
|
|
func (a *App) sendMessage(c *gin.Context) {
|
|
|
|
|
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxMsgLen)
|
|
|
|
|
|
2026-07-08 16:23:00 +08:00
|
|
|
var message sendRequest
|
2026-07-08 15:17:07 +08:00
|
|
|
if err := c.ShouldBind(&message); err != nil {
|
|
|
|
|
a.handleBindError(c, err)
|
|
|
|
|
return
|
2020-05-11 14:58:22 +08:00
|
|
|
}
|
2026-07-08 16:23:00 +08:00
|
|
|
ciimsURL, ok := a.resolveTargetURL(c, message.URL)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-07-08 15:17:07 +08:00
|
|
|
msg := internal.CreateSend(message.User, message.Pass,
|
|
|
|
|
message.Priority, message.Event, message.Val, message.Msg)
|
2026-07-08 16:23:00 +08:00
|
|
|
resp, err := a.config.Client.Send(c.Request.Context(), ciimsURL, msg)
|
2026-07-08 15:17:07 +08:00
|
|
|
if err != nil {
|
|
|
|
|
a.handleSendError(c, "send", resp, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-07-08 16:23:00 +08:00
|
|
|
if errMsg := resp.ErrorMessage(); errMsg != "" {
|
2026-07-08 15:17:07 +08:00
|
|
|
log.Printf("[ERROR] SOAP fault: %s", errMsg)
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": errMsg})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"error": ""})
|
2020-05-11 14:58:22 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-08 15:17:07 +08:00
|
|
|
func (a *App) receiveMessage(c *gin.Context) {
|
|
|
|
|
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxMsgLen)
|
|
|
|
|
|
2026-07-08 16:23:00 +08:00
|
|
|
var message receiveRequest
|
2026-07-08 15:17:07 +08:00
|
|
|
if err := c.ShouldBind(&message); err != nil {
|
|
|
|
|
a.handleBindError(c, err)
|
|
|
|
|
return
|
2020-05-11 14:58:22 +08:00
|
|
|
}
|
2026-07-08 15:17:07 +08:00
|
|
|
|
|
|
|
|
if message.Count < 1 || message.Count > maxCount {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "count must be between 1 and " + strconv.Itoa(maxCount)})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 16:23:00 +08:00
|
|
|
ciimsURL, ok := a.resolveTargetURL(c, message.URL)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-07-08 15:17:07 +08:00
|
|
|
msg := internal.CreateReceive(message.User, message.Pass, message.Count)
|
2026-07-08 16:23:00 +08:00
|
|
|
resp, err := a.config.Client.Send(c.Request.Context(), ciimsURL, msg)
|
2026-07-08 15:17:07 +08:00
|
|
|
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()})
|
2020-05-11 14:58:22 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-08 16:23:00 +08:00
|
|
|
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
|
2020-05-11 14:58:22 +08:00
|
|
|
}
|
2026-07-08 16:23:00 +08:00
|
|
|
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
|
2020-05-11 14:58:22 +08:00
|
|
|
}
|
2026-07-08 15:17:07 +08:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
2026-07-08 16:23:00 +08:00
|
|
|
if ret <= 0 {
|
|
|
|
|
return 0, fmt.Errorf("invalid %s value %q: must be positive", key, val)
|
|
|
|
|
}
|
2026-07-08 15:17:07 +08:00
|
|
|
return ret, nil
|
|
|
|
|
}
|