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:
+25
-4
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user