You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

106 lines
3.3 KiB

package sessionhealth
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"strings"
"time"
"tera-clone/services/tera-api/internal/directstream"
"tera-clone/services/tera-api/internal/serial"
)
const maxStatusResponseBytes = 64 << 10
type Status struct {
Status string `json:"status"`
Broker directstream.BrokerSnapshot `json:"broker"`
WebSocket directstream.WebSocketSnapshot `json:"websocket"`
Tickets directstream.TicketServiceSnapshot `json:"tickets"`
Serial serial.StreamSnapshot `json:"serial"`
TicketError string `json:"ticket_error,omitempty"`
SerialError string `json:"serial_error,omitempty"`
}
type Controller interface {
Status(context.Context) (Status, error)
}
type HTTPClient struct {
endpoint string
client *http.Client
}
func NewHTTPClient(endpoint string) (*HTTPClient, error) {
parsed, err := url.Parse(strings.TrimSpace(endpoint))
if err != nil || parsed.Scheme != "http" || parsed.User != nil || parsed.Host == "" {
return nil, errors.New("sessiond status URL must be an unauthenticated HTTP loopback URL")
}
host := parsed.Hostname()
ip := net.ParseIP(host)
if !strings.EqualFold(host, "localhost") && (ip == nil || !ip.IsLoopback()) {
return nil, errors.New("sessiond status URL host must be loopback")
}
if parsed.RawQuery != "" || parsed.Fragment != "" {
return nil, errors.New("sessiond status URL must not contain a query or fragment")
}
return &HTTPClient{
endpoint: parsed.String(),
client: &http.Client{
Timeout: 2 * time.Second,
CheckRedirect: func(*http.Request, []*http.Request) error {
return errors.New("sessiond status redirects are not allowed")
},
},
}, nil
}
func (c *HTTPClient) Status(ctx context.Context) (Status, error) {
if c == nil || c.client == nil {
return Status{}, errors.New("sessiond status client is unavailable")
}
request, err := http.NewRequestWithContext(ctx, http.MethodGet, c.endpoint, nil)
if err != nil {
return Status{}, fmt.Errorf("create sessiond status request: %w", err)
}
response, err := c.client.Do(request)
if err != nil {
return Status{}, fmt.Errorf("read sessiond status: %w", err)
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusServiceUnavailable {
return Status{}, fmt.Errorf("sessiond status returned HTTP %d", response.StatusCode)
}
decoder := json.NewDecoder(io.LimitReader(response.Body, maxStatusResponseBytes))
decoder.DisallowUnknownFields()
var status Status
if err := decoder.Decode(&status); err != nil {
return Status{}, fmt.Errorf("decode sessiond status: %w", err)
}
var extra any
if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
return Status{}, errors.New("sessiond status must contain one JSON value")
}
if status.Status != "ok" && status.Status != "degraded" {
return Status{}, errors.New("sessiond status value is invalid")
}
return status, nil
}
type UnavailableController struct {
Reason string
}
func (c UnavailableController) Status(context.Context) (Status, error) {
reason := strings.TrimSpace(c.Reason)
if reason == "" {
reason = "sessiond status endpoint is unavailable"
}
return Status{}, errors.New(reason)
}