refactor ciims proxy transport and handlers
This commit is contained in:
+157
-48
@@ -1,97 +1,206 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/labstack/gommon/log"
|
||||
"gzzn.com/mini/ciimsproxy/internal"
|
||||
)
|
||||
|
||||
const (
|
||||
servicePrefix string = "/services/ExchangeService"
|
||||
// defaultURL string = "http://localhost:8080"
|
||||
//defaultURL string = "http://192.168.10.96:8080"
|
||||
servicePrefix string = "/services/ExchangeService"
|
||||
defaultTimeout = 240
|
||||
maxMsgLen = 1048576 // 1MB
|
||||
maxCount = 1000
|
||||
)
|
||||
|
||||
var defaultURL = ""
|
||||
type Config struct {
|
||||
ServerURL string
|
||||
Listen string
|
||||
Timeout int
|
||||
Client *internal.Client
|
||||
}
|
||||
|
||||
type App struct {
|
||||
config Config
|
||||
}
|
||||
|
||||
func main() {
|
||||
defaultURL = os.Getenv("CIIMS_SERVER")
|
||||
listen := os.Getenv("PROXY_LISTEN")
|
||||
if listen == "" {
|
||||
listen = ":9090"
|
||||
config, err := loadConfig()
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] configuration error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
println("use ciims : " + defaultURL)
|
||||
|
||||
log.Printf("use ciims: %s", config.ServerURL)
|
||||
r := newRouter(config)
|
||||
log.Printf("Starting ciims proxy for %s", config.ServerURL)
|
||||
log.Printf("timeout: %d", config.Timeout)
|
||||
if err := r.Run(config.Listen); err != nil {
|
||||
log.Printf("[ERROR] server failed: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func loadConfig() (Config, error) {
|
||||
config := Config{
|
||||
ServerURL: os.Getenv("CIIMS_SERVER"),
|
||||
Listen: os.Getenv("PROXY_LISTEN"),
|
||||
Timeout: defaultTimeout,
|
||||
}
|
||||
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 newRouter(config Config) *gin.Engine {
|
||||
app := &App{config: config}
|
||||
r := gin.Default()
|
||||
r.GET("/ping", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{
|
||||
"message": "pong",
|
||||
})
|
||||
})
|
||||
r.POST("/send", sendMessage)
|
||||
r.POST("/receive", receiveMessage)
|
||||
// r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
|
||||
log.Info("Starting ciims proxy for : " + defaultURL)
|
||||
r.Run(listen)
|
||||
r.POST("/send", app.sendMessage)
|
||||
r.POST("/receive", app.receiveMessage)
|
||||
return r
|
||||
}
|
||||
|
||||
func sendMessage(c *gin.Context) {
|
||||
func (a *App) sendMessage(c *gin.Context) {
|
||||
// Limit request body size
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxMsgLen)
|
||||
|
||||
var message struct {
|
||||
URL string `json:"url" `
|
||||
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" `
|
||||
Priority int `json:"priority"`
|
||||
Val bool `json:"val"`
|
||||
Msg string `json:"msg" binding:"required"`
|
||||
}
|
||||
err := c.Bind(&message)
|
||||
if err == nil {
|
||||
msg := internal.CreateSend(message.User, message.Pass,
|
||||
message.Priority, message.Event, message.Val, message.Msg)
|
||||
url := getURL(message.URL)
|
||||
resp, err := internal.Send(url, msg)
|
||||
errMsg := internal.GetErrMsg(resp)
|
||||
if err == nil {
|
||||
c.JSON(http.StatusOK, gin.H{"error": errMsg})
|
||||
} else {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": errMsg})
|
||||
}
|
||||
} else {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
if err := c.ShouldBind(&message); err != nil {
|
||||
a.handleBindError(c, err)
|
||||
return
|
||||
}
|
||||
msg := internal.CreateSend(message.User, message.Pass,
|
||||
message.Priority, message.Event, message.Val, message.Msg)
|
||||
url := a.getURL(message.URL)
|
||||
resp, err := a.config.Client.Send(url, msg)
|
||||
if err != nil {
|
||||
a.handleSendError(c, "send", resp, err)
|
||||
return
|
||||
}
|
||||
errMsg := resp.ErrorMessage()
|
||||
if len(errMsg) > 0 {
|
||||
log.Printf("[ERROR] SOAP fault: %s", errMsg)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": errMsg})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"error": ""})
|
||||
}
|
||||
|
||||
func receiveMessage(c *gin.Context) {
|
||||
func (a *App) receiveMessage(c *gin.Context) {
|
||||
// Limit request body size
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxMsgLen)
|
||||
|
||||
var message struct {
|
||||
URL string `json:"url" `
|
||||
URL string `json:"url"`
|
||||
User string `json:"user" binding:"required"`
|
||||
Pass string `json:"pass" binding:"required"`
|
||||
Count int `json:"count" binding:"required"`
|
||||
}
|
||||
err := c.Bind(&message)
|
||||
if err == nil {
|
||||
msg := internal.CreateReceive(message.User, message.Pass, message.Count)
|
||||
url := getURL(message.URL)
|
||||
resp, err := internal.Send(url, msg)
|
||||
if err == nil {
|
||||
c.JSON(http.StatusOK, gin.H{"msgs": internal.GetMsgs(resp)})
|
||||
} else {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
} else {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
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
|
||||
}
|
||||
|
||||
msg := internal.CreateReceive(message.User, message.Pass, message.Count)
|
||||
url := a.getURL(message.URL)
|
||||
resp, err := a.config.Client.Send(url, 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 getURL(url string) string {
|
||||
func (a *App) getURL(url string) string {
|
||||
var result string
|
||||
if len(url) > 0 {
|
||||
result = url + servicePrefix
|
||||
} else {
|
||||
result = defaultURL + servicePrefix
|
||||
result = a.config.ServerURL + servicePrefix
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gzzn.com/mini/ciimsproxy/internal"
|
||||
)
|
||||
|
||||
const (
|
||||
testErrMsg = `<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><soap:Fault><detail><BHIAFault><errorMessage xmlns="http://msg.ciims.bhia.itdcl.com">Can not find the event [FLOP-ESTT-ATC-ALL1]</errorMessage></BHIAFault></detail></soap:Fault></soap:Body></soap:Envelope>`
|
||||
testSendOK = `<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><ns1:sendResponse xmlns:ns1="http://ciims.bhia.itdcl.com/ExchangeService" /></soap:Body></soap:Envelope>`
|
||||
testReceiveResp = `<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><ns1:receiveResponse xmlns:ns1="http://ciims.bhia.itdcl.com/ExchangeService"><ns1:out><ns1:string><?xml version="1.0" encoding="UTF-8"?><MSG><A>1</A></MSG></ns1:string></ns1:out></ns1:receiveResponse></soap:Body></soap:Envelope>`
|
||||
)
|
||||
|
||||
func testRouter(t *testing.T, handler http.HandlerFunc) *gin.Engine {
|
||||
t.Helper()
|
||||
gin.SetMode(gin.TestMode)
|
||||
ciims := httptest.NewServer(handler)
|
||||
t.Cleanup(ciims.Close)
|
||||
client, err := internal.NewClient(10)
|
||||
require.NoError(t, err)
|
||||
return newRouter(Config{ServerURL: ciims.URL, Listen: ":0", Timeout: 10, Client: client})
|
||||
}
|
||||
|
||||
func performJSON(r http.Handler, method, path, body string) *httptest.ResponseRecorder {
|
||||
req := httptest.NewRequest(method, path, strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
return w
|
||||
}
|
||||
|
||||
func TestPing(t *testing.T) {
|
||||
r := testRouter(t, func(w http.ResponseWriter, r *http.Request) {})
|
||||
w := performJSON(r, http.MethodGet, "/ping", "")
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.JSONEq(t, `{"message":"pong"}`, w.Body.String())
|
||||
}
|
||||
|
||||
func TestSendSuccess(t *testing.T) {
|
||||
r := testRouter(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, testSendOK)
|
||||
})
|
||||
w := performJSON(r, http.MethodPost, "/send", `{"user":"FIMS","pass":"x","event":"E1","msg":"<MSG/>"}`)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.JSONEq(t, `{"error":""}`, w.Body.String())
|
||||
}
|
||||
|
||||
func TestSendSOAPFault(t *testing.T) {
|
||||
r := testRouter(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, testErrMsg)
|
||||
})
|
||||
w := performJSON(r, http.MethodPost, "/send", `{"user":"FIMS","pass":"x","event":"E1","msg":"<MSG/>"}`)
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "Can not find the event")
|
||||
}
|
||||
|
||||
func TestSendMissingRequiredField(t *testing.T) {
|
||||
r := testRouter(t, func(w http.ResponseWriter, r *http.Request) {})
|
||||
w := performJSON(r, http.MethodPost, "/send", `{"user":"FIMS","pass":"x","msg":"<MSG/>"}`)
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
}
|
||||
|
||||
func TestSendOversizedBody(t *testing.T) {
|
||||
r := testRouter(t, func(w http.ResponseWriter, r *http.Request) {})
|
||||
body := `{"user":"FIMS","pass":"x","event":"E1","msg":"` + strings.Repeat("a", maxMsgLen) + `"}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/send", bytes.NewBufferString(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusRequestEntityTooLarge, w.Code)
|
||||
}
|
||||
|
||||
func TestReceiveSuccess(t *testing.T) {
|
||||
r := testRouter(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, testReceiveResp)
|
||||
})
|
||||
w := performJSON(r, http.MethodPost, "/receive", `{"user":"FIMS","pass":"x","count":2}`)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "MSG")
|
||||
}
|
||||
|
||||
func TestReceiveInvalidCount(t *testing.T) {
|
||||
r := testRouter(t, func(w http.ResponseWriter, r *http.Request) {})
|
||||
|
||||
w := performJSON(r, http.MethodPost, "/receive", `{"user":"FIMS","pass":"x","count":0}`)
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
|
||||
w = performJSON(r, http.MethodPost, "/receive", `{"user":"FIMS","pass":"x","count":1001}`)
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
}
|
||||
|
||||
func TestReceiveSOAPFault(t *testing.T) {
|
||||
r := testRouter(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, testErrMsg)
|
||||
})
|
||||
w := performJSON(r, http.MethodPost, "/receive", `{"user":"FIMS","pass":"x","count":2}`)
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "Can not find the event")
|
||||
}
|
||||
|
||||
func TestHTTP500WithSOAPFaultReturnsFaultText(t *testing.T) {
|
||||
r := testRouter(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprint(w, testErrMsg)
|
||||
})
|
||||
w := performJSON(r, http.MethodPost, "/send", `{"user":"FIMS","pass":"x","event":"E1","msg":"<MSG/>"}`)
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "Can not find the event")
|
||||
assert.NotContains(t, w.Body.String(), "ciims returned 500")
|
||||
}
|
||||
Reference in New Issue
Block a user