refactor ciims proxy transport and handlers

This commit is contained in:
zhiqiang feng
2026-07-08 15:17:07 +08:00
parent 04bafe01e7
commit dd3f6769f6
8 changed files with 689 additions and 154 deletions
+41 -36
View File
@@ -3,7 +3,7 @@ package internal
import (
"bytes"
"encoding/xml"
"regexp"
"io"
"strconv"
"strings"
)
@@ -82,66 +82,71 @@ const (
</soap:Envelope>
`
msgExp = `<ns1:string>.*<\/ns1:string>`
errExp = `<errorMessage .*<\/errorMessage>`
)
// CreateSend xml string for sending
func CreateSend(user string, pass string, priority int, event string, valXML bool, message string) string {
var mb bytes.Buffer
xml.Escape(&mb, []byte(message))
msg := strings.Replace(sendtpl, "##user##", user, -1)
msg = strings.Replace(msg, "##pass##", pass, -1)
msg = strings.Replace(msg, "##event##", event, -1)
msg := strings.Replace(sendtpl, "##user##", xmlEscape(user), -1)
msg = strings.Replace(msg, "##pass##", xmlEscape(pass), -1)
msg = strings.Replace(msg, "##event##", xmlEscape(event), -1)
msg = strings.Replace(msg, "##priority##", strconv.Itoa(priority), -1)
msg = strings.Replace(msg, "##xml##", strconv.FormatBool(valXML), -1)
msg = strings.Replace(msg, "##msg##", mb.String(), -1)
return msg
}
//CreateReceive message
// CreateReceive message
func CreateReceive(user string, pass string, count int) string {
msg := strings.Replace(receivetpl, "##user##", user, -1)
msg = strings.Replace(msg, "##pass##", pass, -1)
msg := strings.Replace(receivetpl, "##user##", xmlEscape(user), -1)
msg = strings.Replace(msg, "##pass##", xmlEscape(pass), -1)
msg = strings.Replace(msg, "##count##", strconv.Itoa(count), -1)
return msg
}
//Map get values
func Map(vs []string, f func(string) string) []string {
vsm := make([]string, len(vs))
for i, v := range vs {
vsm[i] = f(v)
}
return vsm
func xmlEscape(s string) string {
var buf bytes.Buffer
xml.Escape(&buf, []byte(s))
return buf.String()
}
//GetMsgs get message from soap response
// GetMsgs get message from soap response
func GetMsgs(soap string) []string {
r, _ := regexp.Compile(msgExp)
msgs := r.FindAllString(soap, -1)
return Map(msgs, split)
return xmlElementTexts(soap, "string")
}
//GetErrMsg get error message
// GetErrMsg get error message
func GetErrMsg(soap string) string {
r, _ := regexp.Compile(errExp)
msg := r.FindAllString(soap, -1)
if len(msg) < 1 {
return ""
matches := xmlElementTexts(soap, "errorMessage")
if len(matches) > 0 {
return matches[0]
}
if len(msg[0]) > 40 {
size := len(msg[0])
return msg[0][54 : size-15]
}
return msg[0]
return ""
}
func split(msg string) string {
if len(msg) > 12 {
return msg[12 : len(msg)-13]
}
return msg
func xmlElementTexts(soap string, localName string) []string {
decoder := xml.NewDecoder(strings.NewReader(soap))
var result []string
for {
token, err := decoder.Token()
if err == io.EOF {
break
}
if err != nil {
return result
}
start, ok := token.(xml.StartElement)
if !ok || start.Name.Local != localName {
continue
}
var text string
if err := decoder.DecodeElement(&text, &start); err != nil {
return result
}
result = append(result, text)
}
return result
}
+174
View File
@@ -0,0 +1,174 @@
package internal
import (
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestClientSend_SOAPFault(t *testing.T) {
ciims := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/xml; charset=UTF-8")
fmt.Fprint(w, ErrMsg)
}))
defer ciims.Close()
client, err := NewClient(10)
require.NoError(t, err)
resp, err := client.Send(ciims.URL+"/services/ExchangeService", "<dummy>")
assert.NoError(t, err)
require.NotNil(t, resp)
assert.True(t, resp.IsFault())
assert.Equal(t, "Can not find the event [FLOP-ESTT-ATC-ALL1]", resp.ErrorMessage())
}
func TestClientSend_Success(t *testing.T) {
ciims := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/xml; charset=UTF-8")
fmt.Fprint(w, SendOk)
}))
defer ciims.Close()
client, err := NewClient(10)
require.NoError(t, err)
resp, err := client.Send(ciims.URL+"/services/ExchangeService", "<dummy>")
assert.NoError(t, err)
require.NotNil(t, resp)
assert.Equal(t, "", resp.ErrorMessage())
}
func TestClientSend_ReceiveMessages(t *testing.T) {
ciims := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/xml; charset=UTF-8")
fmt.Fprint(w, ReceiveResp)
}))
defer ciims.Close()
client, err := NewClient(10)
require.NoError(t, err)
resp, err := client.Send(ciims.URL+"/services/ExchangeService", "<dummy>")
assert.NoError(t, err)
require.NotNil(t, resp)
msgs := resp.Messages()
assert.Equal(t, 2, len(msgs))
assert.Equal(t, Msg, msgs[0])
assert.Equal(t, Msg, msgs[1])
}
func TestClientSend_NetworkTimeout(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
addr := listener.Addr().String()
go func() {
conn, _ := listener.Accept()
if conn != nil {
io.Copy(io.Discard, conn)
conn.Close()
}
}()
client, err := NewClient(1)
require.NoError(t, err)
_, err = client.Send("http://"+addr+"/services/ExchangeService", "<dummy>")
listener.Close()
assert.Error(t, err)
}
func TestClientSend_ServerError(t *testing.T) {
client, err := NewClient(1)
require.NoError(t, err)
_, err = client.Send("http://127.0.0.1:1/nonexistent", "<dummy>")
assert.Error(t, err)
}
func TestClientSend_HTTPStatusError(t *testing.T) {
ciims := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "backend failed", http.StatusInternalServerError)
}))
defer ciims.Close()
client, err := NewClient(10)
require.NoError(t, err)
resp, err := client.Send(ciims.URL+"/services/ExchangeService", "<dummy>")
require.Error(t, err)
require.NotNil(t, resp)
var statusErr *HTTPStatusError
require.True(t, errors.As(err, &statusErr))
assert.Equal(t, http.StatusInternalServerError, statusErr.StatusCode)
assert.Contains(t, statusErr.Body, "backend failed")
}
func TestGetMsgs_Empty(t *testing.T) {
msgs := GetMsgs("<soap><body></body></soap>")
assert.Equal(t, 0, len(msgs))
}
func TestGetMsgs_DifferentNamespacePrefix(t *testing.T) {
resp := `<soap:Envelope><soap:Body><ns2:receiveResponse xmlns:ns2="http://ciims.bhia.itdcl.com/ExchangeService">
<ns2:out><ns2:string>&lt;MSG/&gt;</ns2:string></ns2:out>
</ns2:receiveResponse></soap:Body></soap:Envelope>`
msgs := GetMsgs(resp)
assert.Equal(t, 1, len(msgs))
assert.Equal(t, "<MSG/>", msgs[0])
}
func TestGetMsgs_AttributesWhitespaceAndMultiline(t *testing.T) {
resp := `<soap:Envelope><soap:Body><ns2:out>
<ns2:string id="1">
&lt;MSG&gt;
&lt;A&gt;1&lt;/A&gt;
&lt;/MSG&gt;
</ns2:string>
</ns2:out></soap:Body></soap:Envelope>`
msgs := GetMsgs(resp)
require.Len(t, msgs, 1)
assert.Contains(t, msgs[0], "<MSG>")
assert.Contains(t, msgs[0], "<A>1</A>")
}
func TestGetErrMsg_NoError(t *testing.T) {
errMsg := GetErrMsg(SendOk)
assert.Equal(t, "", errMsg)
}
func TestGetErrMsg_Empty(t *testing.T) {
errMsg := GetErrMsg("<soap><body></body></soap>")
assert.Equal(t, "", errMsg)
}
func TestGetErrMsg_AttributesWhitespaceAndMultiline(t *testing.T) {
resp := `<soap:Envelope><soap:Body><soap:Fault>
<errorMessage xmlns="http://msg.ciims.bhia.itdcl.com" code="20">
Can not find
the event
</errorMessage>
</soap:Fault></soap:Body></soap:Envelope>`
errMsg := GetErrMsg(resp)
assert.Contains(t, errMsg, "Can not find")
assert.Contains(t, errMsg, "the event")
}
func TestCreateSend_XMLEscapes(t *testing.T) {
result := CreateSend("user<>&\"'", "pass", 0, "event", false, "<MSG/>")
assert.Contains(t, result, "user&lt;&gt;&amp;&#34;&#39;")
assert.Contains(t, result, "pass")
assert.Contains(t, result, "&lt;MSG/&gt;")
}
func TestCreateReceive_XMLEscapes(t *testing.T) {
result := CreateReceive("user<>&\"'", "pass>", 2)
assert.Contains(t, result, "user&lt;&gt;&amp;&#34;&#39;")
assert.Contains(t, result, "pass&gt;")
}
+69 -21
View File
@@ -1,8 +1,8 @@
package internal
import (
"bytes"
"io/ioutil"
"fmt"
"io"
"net/http"
"strings"
"time"
@@ -13,37 +13,85 @@ const (
defaultAgent string = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; XFire Client +http://xfire.codehaus.org)"
)
//Post xml string to url
func post(url string, contentType string, msg string) (*http.Response, error) {
return http.Post(url, contentType, bytes.NewBuffer([]byte(msg)))
type Client struct {
httpClient *http.Client
}
func postSim(url string, msg string) (string, error) {
timeout := time.Duration(10 * time.Second)
client := http.Client{
Timeout: timeout,
}
type CIIMSResponse struct {
RawXML string
}
type HTTPStatusError struct {
StatusCode int
Status string
Body string
}
func (e *HTTPStatusError) Error() string {
body := e.Body
if len(body) > 512 {
body = body[:512] + "..."
}
if body == "" {
return fmt.Sprintf("ciims returned %s", e.Status)
}
return fmt.Sprintf("ciims returned %s: %s", e.Status, body)
}
func NewClient(timeoutSec int) (*Client, error) {
if timeoutSec <= 0 {
return nil, fmt.Errorf("timeout must be positive")
}
return &Client{
httpClient: &http.Client{
Timeout: time.Duration(timeoutSec) * time.Second,
},
}, nil
}
func (r *CIIMSResponse) IsFault() bool {
return r != nil && r.ErrorMessage() != ""
}
func (r *CIIMSResponse) ErrorMessage() string {
if r == nil {
return ""
}
return GetErrMsg(r.RawXML)
}
func (r *CIIMSResponse) Messages() []string {
if r == nil {
return nil
}
return GetMsgs(r.RawXML)
}
func (c *Client) Send(url string, msg string) (*CIIMSResponse, error) {
req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(msg))
if err != nil {
return err.Error(), err
return nil, err
}
req.Header.Set("Content-Type", defaultContentType)
req.Header.Set("User-Agent", defaultAgent)
req.Header.Set("SOAPAction", "")
resp, err := client.Do(req)
resp, err := c.httpClient.Do(req)
if err != nil {
return err.Error(), err
return nil, err
}
defer resp.Body.Close()
respBytes, err := ioutil.ReadAll(resp.Body)
respBytes, err := io.ReadAll(resp.Body)
if err != nil {
return err.Error(), err
return nil, err
}
return string(respBytes), nil
}
//Send message
func Send(url string, message string) (string, error) {
return postSim(url, message)
body := string(respBytes)
ciimsResp := &CIIMSResponse{RawXML: body}
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
return ciimsResp, &HTTPStatusError{
StatusCode: resp.StatusCode,
Status: resp.Status,
Body: body,
}
}
return ciimsResp, nil
}