fix: comprehensive bug fixes, hardening, and test coverage

Bug fixes:
- Fix nil-pointer panic in sendMessage SOAP fault handler (used err.Error() on nil)
- Fix missing return after SOAP fault response (caused fall-through to 200 OK)
- Fix timeout=0 shadowing: removed package-level constant, use configured value
- Fix receiveMessage not checking SOAP faults from CIIMS

Codec hardening:
- Replace fragile regex parsing with encoding/xml.Decoder for namespace-agnostic XML
- XML-escape all user inputs (user, pass, event) — not just message body
- Remove magic-number-based string slicing (split, GetErrMsg)

Transport improvements:
- Extract Client type with connection pooling (reuse http.Client across requests)
- Add CIIMSResponse domain type with IsFault(), ErrorMessage(), Messages()
- Add HTTPStatusError and ResponseTooLargeError typed errors
- Replace deprecated ioutil.ReadAll with io.ReadAll
- Add response body size limit (64 MiB)

Security hardening:
- Add MaxBytesReader (1MB request body limit) on both endpoints
- Add URL allowlist via CIIMS_ALLOWED_SERVERS env var
- Add normalizeBaseURL with scheme/host/query validation
- Add count bounds validation (1-1000) on /receive
- Add server-level timeouts (ReadHeaderTimeout, ReadTimeout, IdleTimeout)

Configuration:
- Introduce Config struct to replace package-level globals
- Add loadConfig() with full validation and error propagation
- Add getIntEnv() with positive-value enforcement

Test coverage (75 tests, 35 new):
- Phase 1: 8 internal tests (nil receiver, error types, SOAP+HTTP500, malformed XML)
- Phase 2: 19 pure function tests (normalizeBaseURL, parseAllowedServers, getIntEnv)
- Phase 3: 16 handler tests (send/receive success, SOAP fault, network error, HTTP 500,
  body too large, invalid JSON, missing fields, URL allowlist)
- Phase 4: 8 config/router tests (loadConfig, newServer, newRouter)

Toolchain:
- Upgrade Go 1.13 → 1.22, gin 1.6.3 → 1.10.0, testify 1.5.1 → 1.10.0

Cleanup:
- Remove dead code (unused post() function, commented-out defaults)
- Replace println with structured logging
- Add .gitignore
- Rewrite README with API docs, env vars, security considerations
This commit is contained in:
zhiqiang feng
2026-07-08 16:23:00 +08:00
parent dd3f6769f6
commit ce4c2b088f
12 changed files with 1704 additions and 85 deletions
+25 -4
View File
@@ -1,6 +1,7 @@
package internal
import (
"context"
"fmt"
"io"
"net/http"
@@ -11,6 +12,7 @@ import (
const (
defaultContentType string = "text/xml; charset=UTF-8"
defaultAgent string = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; XFire Client +http://xfire.codehaus.org)"
maxResponseBytes = 64 << 20 // 64 MiB
)
type Client struct {
@@ -27,6 +29,10 @@ type HTTPStatusError struct {
Body string
}
type ResponseTooLargeError struct {
Limit int64
}
func (e *HTTPStatusError) Error() string {
body := e.Body
if len(body) > 512 {
@@ -38,6 +44,10 @@ func (e *HTTPStatusError) Error() string {
return fmt.Sprintf("ciims returned %s: %s", e.Status, body)
}
func (e *ResponseTooLargeError) Error() string {
return fmt.Sprintf("ciims response exceeds %d bytes", e.Limit)
}
func NewClient(timeoutSec int) (*Client, error) {
if timeoutSec <= 0 {
return nil, fmt.Errorf("timeout must be positive")
@@ -67,8 +77,8 @@ func (r *CIIMSResponse) Messages() []string {
return GetMsgs(r.RawXML)
}
func (c *Client) Send(url string, msg string) (*CIIMSResponse, error) {
req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(msg))
func (c *Client) Send(ctx context.Context, url string, msg string) (*CIIMSResponse, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, strings.NewReader(msg))
if err != nil {
return nil, err
}
@@ -80,11 +90,10 @@ func (c *Client) Send(url string, msg string) (*CIIMSResponse, error) {
return nil, err
}
defer resp.Body.Close()
respBytes, err := io.ReadAll(resp.Body)
body, err := readLimited(resp.Body, maxResponseBytes)
if err != nil {
return nil, err
}
body := string(respBytes)
ciimsResp := &CIIMSResponse{RawXML: body}
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
return ciimsResp, &HTTPStatusError{
@@ -95,3 +104,15 @@ func (c *Client) Send(url string, msg string) (*CIIMSResponse, error) {
}
return ciimsResp, nil
}
func readLimited(r io.Reader, limit int64) (string, error) {
limited := io.LimitReader(r, limit+1)
respBytes, err := io.ReadAll(limited)
if err != nil {
return "", err
}
if int64(len(respBytes)) > limit {
return "", &ResponseTooLargeError{Limit: limit}
}
return string(respBytes), nil
}