Skip to content

Commit

Permalink
Merge pull request #2 from beeper/tulir/relay-mode
Browse files Browse the repository at this point in the history
Add relay mode
  • Loading branch information
tulir authored Dec 18, 2023
2 parents 357db77 + 98ff9ec commit a2ebf6c
Show file tree
Hide file tree
Showing 8 changed files with 406 additions and 145 deletions.
58 changes: 58 additions & 0 deletions generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package main

import (
"context"
"fmt"
"time"

"github.com/beeper/nacserv-native/nac"
"github.com/beeper/nacserv-native/requests"
)

var globalCert []byte

func InitFetchCert(ctx context.Context) error {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, 30*time.Second)
defer cancel()
var err error
globalCert, err = requests.FetchCert(ctx)
if err != nil {
return fmt.Errorf("failed to fetch cert: %w", err)
}
return nil
}

func InitSanityCheck() error {
defer nac.MeowMemory()()
return nac.SanityCheck()
}

const ValidityTime = 15 * time.Minute

func GenerateValidationData(ctx context.Context) ([]byte, time.Time, error) {
defer nac.MeowMemory()()

validationCtx, request, err := nac.Init(globalCert)
if err != nil {
return nil, time.Time{}, err
}
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, 30*time.Second)
// Record valid until time before request, so it's definitely valid for at least that long
validUntil := time.Now().UTC().Add(ValidityTime)
sessionInfo, err := requests.InitializeValidation(ctx, request)
cancel()
if err != nil {
return nil, validUntil, fmt.Errorf("failed to initialize validation: %w", err)
}
err = nac.KeyEstablishment(validationCtx, sessionInfo)
if err != nil {
return nil, validUntil, err
}
validationData, err := nac.Sign(validationCtx)
if err != nil {
return nil, validUntil, err
}
return validationData, validUntil, nil
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.21
require (
github.com/tidwall/gjson v1.17.0
howett.net/plist v1.0.0
nhooyr.io/websocket v1.8.10
)

require (
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q=
nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
164 changes: 26 additions & 138 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,24 +1,19 @@
package main

import (
"bytes"
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
"runtime"
"runtime/debug"
"strings"
"sync"
"time"

"github.com/beeper/nacserv-native/nac"
"github.com/beeper/nacserv-native/requests"
"github.com/beeper/nacserv-native/versions"
)

Expand All @@ -31,18 +26,20 @@ type ReqSubmitValidationData struct {

var Commit = "unknown "

var submitToken = flag.String("token", "", "Token to include when submitting validation data")
var submitInterval = flag.Duration("interval", 3*time.Minute, "Interval at which to submit new validation data to the server")
var submitUserAgent = fmt.Sprintf("nacserv-native/%s go/%s macOS/%s", Commit[:8], strings.TrimPrefix(runtime.Version(), "go"), versions.Current.SoftwareVersion)
var submitToken = flag.String("submit-token", "", "Token to include when submitting validation data")
var submitInterval = flag.Duration("submit-interval", 0, "Interval at which to submit new validation data to the server")
var relayServer = flag.String("relay-server", "https://imrelay.beeper.com", "URL of the relay server to use")
var jsonOutput = flag.Bool("json", false, "Output JSON instead of text")
var submitUserAgent = fmt.Sprintf("mac-validation-provider/%s go/%s macOS/%s", Commit[:8], strings.TrimPrefix(runtime.Version(), "go"), versions.Current.SoftwareVersion)
var once = flag.Bool("once", false, "Generate a single validation data, print it to stdout and exit")

func main() {
flag.Parse()
var urls []string
if !*once {
if *submitInterval > 0 {
urls = flag.Args()
if len(urls) == 0 {
_, _ = fmt.Fprintln(os.Stderr, "You must pass one or more URLs to submit to when not using -once")
_, _ = fmt.Fprintln(os.Stderr, "You must pass one or more URLs to submit to when using -interval")
return
}
for _, u := range urls {
Expand All @@ -53,29 +50,33 @@ func main() {
panic(fmt.Errorf("unexpected URL scheme %q", parsedURL.Scheme))
}
}
} else {
log.SetOutput(io.Discard)
}

log.Printf("Starting nacserv-native %s", Commit[:8])
log.Println("Loading identityservicesd")
err := nac.Load()
if err != nil {
if *jsonOutput && errors.Is(err, nac.ErrNoOffsets) {
_ = json.NewEncoder(os.Stdout).Encode(map[string]any{
"error": "no offsets",
})
return
}
panic(err)
}
log.Println("Running sanity check...")
err = initSanityCheck()
err = InitSanityCheck()
if err != nil {
panic(err)
}
log.Println("Fetching certificate...")
err = initFetchCert(context.Background())
err = InitFetchCert(context.Background())
if err != nil {
panic(err)
}
log.Println("Initialization complete")
if *once {
validationData, validUntil, err := generateValidationData(context.Background())
validationData, validUntil, err := GenerateValidationData(context.Background())
if err != nil {
panic(err)
}
Expand All @@ -87,129 +88,16 @@ func main() {
})
return
}
for {
generateAndSubmit(urls)
}
}

var panicCounter = 0

func generateAndSubmit(urls []string) {
defer func() {
err := recover()
if *submitInterval > 0 {
log.Printf("Submit mode: periodically submitting validation data to %+v", urls)
for {
generateAndSubmit(urls)
}
} else if len(*relayServer) > 0 {
log.Printf("Relay mode: responding to requests over websocket at %s", *relayServer)
err = ConnectRelay(context.Background(), *relayServer)
if err != nil {
panicCounter++
log.Printf("Panic while generating validation data: %v\n%s", err, debug.Stack())
sleepDuration := time.Duration(panicCounter) * 5 * time.Minute
log.Println("Sleeping for", sleepDuration)
time.Sleep(sleepDuration)
log.Printf("Error in relay connection: %v", err)
}
}()
log.Println("Generating validation data...")
if validationData, validUntil, err := generateValidationData(context.Background()); err != nil {
log.Printf("Failed to generate validation data: %v", err)
} else {
submitValidationDataToURLs(context.Background(), urls, validationData, validUntil)
}
panicCounter = 0
time.Sleep(*submitInterval)
}

func submitValidationDataToURLs(ctx context.Context, urls []string, data []byte, validUntil time.Time) {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, 30*time.Second)
defer cancel()
var wg sync.WaitGroup
wg.Add(len(urls))
for _, u := range urls {
go func(addr string) {
defer wg.Done()
err := submitValidationData(ctx, addr, data, validUntil)
if err != nil {
log.Printf("Failed to submit validation data to %s: %v", addr, err)
} else {
log.Println("Submitted validation data to", addr)
}
}(u)
}
wg.Wait()
}

func submitValidationData(ctx context.Context, url string, data []byte, validUntil time.Time) error {
var buf bytes.Buffer
err := json.NewEncoder(&buf).Encode(&ReqSubmitValidationData{
ValidationData: data,
ValidUntil: validUntil,
NacservCommit: Commit,
DeviceInfo: versions.Current,
})
if err != nil {
return fmt.Errorf("failed to encode request payload: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &buf)
if err != nil {
return fmt.Errorf("failed to prepare request: %w", err)
}
req.Header.Set("User-Agent", submitUserAgent)
req.Header.Set("Content-Type", "application/json")
if len(*submitToken) > 0 {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *submitToken))
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("failed to send request: %w", err)
}
_ = resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code %d", resp.StatusCode)
}
return nil
}

var globalCert []byte

func initFetchCert(ctx context.Context) error {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, 30*time.Second)
defer cancel()
var err error
globalCert, err = requests.FetchCert(ctx)
if err != nil {
return fmt.Errorf("failed to fetch cert: %w", err)
}
return nil
}

func initSanityCheck() error {
defer nac.MeowMemory()()
return nac.SanityCheck()
}

const ValidityTime = 15 * time.Minute

func generateValidationData(ctx context.Context) ([]byte, time.Time, error) {
defer nac.MeowMemory()()

validationCtx, request, err := nac.Init(globalCert)
if err != nil {
return nil, time.Time{}, err
}
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, 30*time.Second)
// Record valid until time before request, so it's definitely valid for at least that long
validUntil := time.Now().UTC().Add(ValidityTime)
sessionInfo, err := requests.InitializeValidation(ctx, request)
cancel()
if err != nil {
return nil, validUntil, fmt.Errorf("failed to initialize validation: %w", err)
}
err = nac.KeyEstablishment(validationCtx, sessionInfo)
if err != nil {
return nil, validUntil, err
}
validationData, err := nac.Sign(validationCtx)
if err != nil {
return nil, validUntil, err
}
return validationData, validUntil, nil
}
5 changes: 4 additions & 1 deletion nac/nac.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package nac
import "C"
import (
"crypto/sha256"
"errors"
"fmt"
"io"
"os"
Expand All @@ -33,14 +34,16 @@ func sha256sum(path string) (hash [32]byte, err error) {
return
}

var ErrNoOffsets = errors.New("no offsets")

func Load() error {
hash, err := sha256sum(identityservicesd)
if err != nil {
return err
}
offs, ok := offsets[hash]
if !ok || offs.ReferenceSymbol == "" {
return fmt.Errorf("no offsets for %x", hash[:])
return fmt.Errorf("%w for %x", ErrNoOffsets, hash[:])
}

handle := C.dlopen(C.CString(identityservicesd), C.RTLD_LAZY)
Expand Down
Loading

0 comments on commit a2ebf6c

Please sign in to comment.