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
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)
|
|
}
|
|
|