 9bdcbe0447
			
		
	
	9bdcbe0447
	
	
	
		
			
			Major integrations and fixes: - Added BACKBEAT SDK integration for P2P operation timing - Implemented beat-aware status tracking for distributed operations - Added Docker secrets support for secure license management - Resolved KACHING license validation via HTTPS/TLS - Updated docker-compose configuration for clean stack deployment - Disabled rollback policies to prevent deployment failures - Added license credential storage (CHORUS-DEV-MULTI-001) Technical improvements: - BACKBEAT P2P operation tracking with phase management - Enhanced configuration system with file-based secrets - Improved error handling for license validation - Clean separation of KACHING and CHORUS deployment stacks 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
		
			
				
	
	
		
			175 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			175 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package ssdp
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"errors"
 | |
| 	"log"
 | |
| 	"net/http"
 | |
| 	"net/url"
 | |
| 	"strconv"
 | |
| 	"time"
 | |
| )
 | |
| 
 | |
| const (
 | |
| 	ssdpDiscover   = `"ssdp:discover"`
 | |
| 	ntsAlive       = `ssdp:alive`
 | |
| 	ntsByebye      = `ssdp:byebye`
 | |
| 	ntsUpdate      = `ssdp:update`
 | |
| 	ssdpUDP4Addr   = "239.255.255.250:1900"
 | |
| 	ssdpSearchPort = 1900
 | |
| 	methodSearch   = "M-SEARCH"
 | |
| 	methodNotify   = "NOTIFY"
 | |
| 
 | |
| 	// SSDPAll is a value for searchTarget that searches for all devices and services.
 | |
| 	SSDPAll = "ssdp:all"
 | |
| 	// UPNPRootDevice is a value for searchTarget that searches for all root devices.
 | |
| 	UPNPRootDevice = "upnp:rootdevice"
 | |
| )
 | |
| 
 | |
| // HTTPUClient is the interface required to perform HTTP-over-UDP requests.
 | |
| type HTTPUClient interface {
 | |
| 	Do(
 | |
| 		req *http.Request,
 | |
| 		timeout time.Duration,
 | |
| 		numSends int,
 | |
| 	) ([]*http.Response, error)
 | |
| }
 | |
| 
 | |
| // HTTPUClientCtx is an optional interface that will be used to perform
 | |
| // HTTP-over-UDP requests if the client implements it.
 | |
| type HTTPUClientCtx interface {
 | |
| 	DoWithContext(
 | |
| 		req *http.Request,
 | |
| 		numSends int,
 | |
| 	) ([]*http.Response, error)
 | |
| }
 | |
| 
 | |
| // SSDPRawSearchCtx performs a fairly raw SSDP search request, and returns the
 | |
| // unique response(s) that it receives. Each response has the requested
 | |
| // searchTarget, a USN, and a valid location. maxWaitSeconds states how long to
 | |
| // wait for responses in seconds, and must be a minimum of 1 (the
 | |
| // implementation waits an additional 100ms for responses to arrive), 2 is a
 | |
| // reasonable value for this. numSends is the number of requests to send - 3 is
 | |
| // a reasonable value for this.
 | |
| func SSDPRawSearchCtx(
 | |
| 	ctx context.Context,
 | |
| 	httpu HTTPUClient,
 | |
| 	searchTarget string,
 | |
| 	maxWaitSeconds int,
 | |
| 	numSends int,
 | |
| ) ([]*http.Response, error) {
 | |
| 	req, err := prepareRequest(ctx, searchTarget, maxWaitSeconds)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	allResponses, err := httpu.Do(req, time.Duration(maxWaitSeconds)*time.Second+100*time.Millisecond, numSends)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	return processSSDPResponses(searchTarget, allResponses)
 | |
| }
 | |
| 
 | |
| // RawSearch performs a fairly raw SSDP search request, and returns the
 | |
| // unique response(s) that it receives. Each response has the requested
 | |
| // searchTarget, a USN, and a valid location. If the provided context times out
 | |
| // or is canceled, the search will be aborted. numSends is the number of
 | |
| // requests to send - 3 is a reasonable value for this.
 | |
| //
 | |
| // The provided context should have a deadline, since the SSDP protocol
 | |
| // requires the max wait time be included in search requests. If the context
 | |
| // has no deadline, then a default deadline of 3 seconds will be applied.
 | |
| func RawSearch(
 | |
| 	ctx context.Context,
 | |
| 	httpu HTTPUClientCtx,
 | |
| 	searchTarget string,
 | |
| 	numSends int,
 | |
| ) ([]*http.Response, error) {
 | |
| 	// We need a timeout value to include in the SSDP request; get it by
 | |
| 	// checking the deadline on the context.
 | |
| 	var maxWaitSeconds int
 | |
| 	if deadline, ok := ctx.Deadline(); ok {
 | |
| 		maxWaitSeconds = int(deadline.Sub(time.Now()) / time.Second)
 | |
| 	} else {
 | |
| 		// Pick a default timeout of 3 seconds if none was provided.
 | |
| 		maxWaitSeconds = 3
 | |
| 
 | |
| 		var cancel func()
 | |
| 		ctx, cancel = context.WithTimeout(ctx, time.Duration(maxWaitSeconds)*time.Second)
 | |
| 		defer cancel()
 | |
| 	}
 | |
| 
 | |
| 	req, err := prepareRequest(ctx, searchTarget, maxWaitSeconds)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	allResponses, err := httpu.DoWithContext(req, numSends)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	return processSSDPResponses(searchTarget, allResponses)
 | |
| }
 | |
| 
 | |
| // prepareRequest checks the provided parameters and constructs a SSDP search
 | |
| // request to be sent.
 | |
| func prepareRequest(ctx context.Context, searchTarget string, maxWaitSeconds int) (*http.Request, error) {
 | |
| 	if maxWaitSeconds < 1 {
 | |
| 		return nil, errors.New("ssdp: request timeout must be at least 1s")
 | |
| 	}
 | |
| 
 | |
| 	req := (&http.Request{
 | |
| 		Method: methodSearch,
 | |
| 		// TODO: Support both IPv4 and IPv6.
 | |
| 		Host: ssdpUDP4Addr,
 | |
| 		URL:  &url.URL{Opaque: "*"},
 | |
| 		Header: http.Header{
 | |
| 			// Putting headers in here avoids them being title-cased.
 | |
| 			// (The UPnP discovery protocol uses case-sensitive headers)
 | |
| 			"HOST": []string{ssdpUDP4Addr},
 | |
| 			"MX":   []string{strconv.FormatInt(int64(maxWaitSeconds), 10)},
 | |
| 			"MAN":  []string{ssdpDiscover},
 | |
| 			"ST":   []string{searchTarget},
 | |
| 		},
 | |
| 	}).WithContext(ctx)
 | |
| 	return req, nil
 | |
| }
 | |
| 
 | |
| func processSSDPResponses(
 | |
| 	searchTarget string,
 | |
| 	allResponses []*http.Response,
 | |
| ) ([]*http.Response, error) {
 | |
| 	isExactSearch := searchTarget != SSDPAll && searchTarget != UPNPRootDevice
 | |
| 
 | |
| 	seenIDs := make(map[string]bool)
 | |
| 	var responses []*http.Response
 | |
| 	for _, response := range allResponses {
 | |
| 		if response.StatusCode != 200 {
 | |
| 			log.Printf("ssdp: got response status code %q in search response", response.Status)
 | |
| 			continue
 | |
| 		}
 | |
| 		if st := response.Header.Get("ST"); isExactSearch && st != searchTarget {
 | |
| 			continue
 | |
| 		}
 | |
| 		usn := response.Header.Get("USN")
 | |
| 		loc, err := response.Location()
 | |
| 		if err != nil {
 | |
| 			// No usable location in search response - discard.
 | |
| 			continue
 | |
| 		}
 | |
| 		id := loc.String() + "\x00" + usn
 | |
| 		if _, alreadySeen := seenIDs[id]; !alreadySeen {
 | |
| 			seenIDs[id] = true
 | |
| 			responses = append(responses, response)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return responses, nil
 | |
| }
 | |
| 
 | |
| // SSDPRawSearch is the legacy version of SSDPRawSearchCtx, but uses
 | |
| // context.Background() as the context.
 | |
| func SSDPRawSearch(httpu HTTPUClient, searchTarget string, maxWaitSeconds int, numSends int) ([]*http.Response, error) {
 | |
| 	return SSDPRawSearchCtx(context.Background(), httpu, searchTarget, maxWaitSeconds, numSends)
 | |
| }
 |