boulder/test/pardot-test-srv/main.go

219 lines
5.6 KiB
Go

package main
import (
"crypto/rand"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
"slices"
"sync"
"time"
"github.com/letsencrypt/boulder/cmd"
)
var contactsCap = 20
type config struct {
// OAuthAddr is the address (e.g. IP:port) on which the OAuth server will
// listen.
OAuthAddr string
// PardotAddr is the address (e.g. IP:port) on which the Pardot server will
// listen.
PardotAddr string
// ExpectedClientID is the client ID that the server expects to receive in
// requests to the /services/oauth2/token endpoint.
ExpectedClientID string `validate:"required"`
// ExpectedClientSecret is the client secret that the server expects to
// receive in requests to the /services/oauth2/token endpoint.
ExpectedClientSecret string `validate:"required"`
}
type contacts struct {
sync.Mutex
created []string
}
type testServer struct {
expectedClientID string
expectedClientSecret string
token string
contacts contacts
}
func (ts *testServer) getTokenHandler(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
clientID := r.FormValue("client_id")
clientSecret := r.FormValue("client_secret")
if clientID != ts.expectedClientID || clientSecret != ts.expectedClientSecret {
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
return
}
response := map[string]interface{}{
"access_token": ts.token,
"token_type": "Bearer",
"expires_in": 3600,
}
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(response)
if err != nil {
log.Printf("Failed to encode token response: %v", err)
http.Error(w, "Failed to encode token response", http.StatusInternalServerError)
}
}
func (ts *testServer) checkToken(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token != "Bearer "+ts.token {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
}
func (ts *testServer) createContactsHandler(w http.ResponseWriter, r *http.Request) {
ts.checkToken(w, r)
businessUnitId := r.Header.Get("Pardot-Business-Unit-Id")
if businessUnitId == "" {
http.Error(w, "Missing 'Pardot-Business-Unit-Id' header", http.StatusBadRequest)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read request body", http.StatusInternalServerError)
return
}
type contactData struct {
Email string `json:"email"`
}
var contact contactData
err = json.Unmarshal(body, &contact)
if err != nil {
http.Error(w, "Failed to parse request body", http.StatusBadRequest)
return
}
if contact.Email == "" {
http.Error(w, "Missing 'email' field in request body", http.StatusBadRequest)
return
}
ts.contacts.Lock()
if len(ts.contacts.created) >= contactsCap {
// Copying the slice in memory is inefficient, but this is a test server
// with a small number of contacts, so it's fine.
ts.contacts.created = ts.contacts.created[1:]
}
ts.contacts.created = append(ts.contacts.created, contact.Email)
ts.contacts.Unlock()
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status": "success"}`))
}
func (ts *testServer) queryContactsHandler(w http.ResponseWriter, r *http.Request) {
ts.checkToken(w, r)
ts.contacts.Lock()
respContacts := slices.Clone(ts.contacts.created)
ts.contacts.Unlock()
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(map[string]interface{}{"contacts": respContacts})
if err != nil {
log.Printf("Failed to encode contacts query response: %v", err)
http.Error(w, "Failed to encode contacts query response", http.StatusInternalServerError)
}
}
func main() {
oauthAddr := flag.String("oauth-addr", "", "OAuth server listen address override")
pardotAddr := flag.String("pardot-addr", "", "Pardot server listen address override")
configFile := flag.String("config", "", "Path to configuration file")
flag.Parse()
if *configFile == "" {
flag.Usage()
os.Exit(1)
}
var c config
err := cmd.ReadConfigFile(*configFile, &c)
cmd.FailOnError(err, "Reading JSON config file into config structure")
if *oauthAddr != "" {
c.OAuthAddr = *oauthAddr
}
if *pardotAddr != "" {
c.PardotAddr = *pardotAddr
}
tokenBytes := make([]byte, 32)
_, err = rand.Read(tokenBytes)
if err != nil {
log.Fatalf("Failed to generate token: %v", err)
}
ts := &testServer{
expectedClientID: c.ExpectedClientID,
expectedClientSecret: c.ExpectedClientSecret,
token: fmt.Sprintf("%x", tokenBytes),
contacts: contacts{created: make([]string, 0, contactsCap)},
}
// OAuth Server
oauthMux := http.NewServeMux()
oauthMux.HandleFunc("/services/oauth2/token", ts.getTokenHandler)
oauthServer := &http.Server{
Addr: c.OAuthAddr,
Handler: oauthMux,
ReadTimeout: 30 * time.Second,
}
log.Printf("pardot-test-srv OAuth server listening at %s", c.OAuthAddr)
go func() {
err := oauthServer.ListenAndServe()
if err != nil {
log.Fatalf("Failed to start OAuth server: %s", err)
}
}()
// Pardot API Server
pardotMux := http.NewServeMux()
pardotMux.HandleFunc("/api/v5/objects/prospects", ts.createContactsHandler)
pardotMux.HandleFunc("/contacts", ts.queryContactsHandler)
pardotServer := &http.Server{
Addr: c.PardotAddr,
Handler: pardotMux,
ReadTimeout: 30 * time.Second,
}
log.Printf("pardot-test-srv Pardot API server listening at %s", c.PardotAddr)
go func() {
err := pardotServer.ListenAndServe()
if err != nil {
log.Fatalf("Failed to start Pardot API server: %s", err)
}
}()
cmd.WaitForSignal()
}