Skip to content

Commit 282553e

Browse files
committed
vcsim: Add minimal json-rpc support
Signed-off-by: Doug MacEachern <[email protected]>
1 parent 8d78f54 commit 282553e

File tree

6 files changed

+194
-13
lines changed

6 files changed

+194
-13
lines changed

govc/test/vcsim.bats

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,3 +585,74 @@ EOF
585585

586586
rm "$file"
587587
}
588+
589+
@test "vcsim jsonrpc" {
590+
vcsim_start
591+
592+
url=$(govc env -x GOVC_URL) # w/o username:password@
593+
594+
run govc session.login -u "$url" -X POST /api <<EOF
595+
{
596+
"jsonrpc": "2.0",
597+
"method": "invoke",
598+
"params": {
599+
"serviceId": "com.vmware.cis.session",
600+
"operationId": "create",
601+
"ctx": {
602+
"securityCtx": {
603+
"schemeId": "com.vmware.vapi.std.security.user_pass",
604+
"userName": "$(govc env GOVC_USERNAME)",
605+
"password": "$(govc env GOVC_PASSWORD)"
606+
}
607+
}
608+
},
609+
"id": "0"
610+
}
611+
EOF
612+
assert_success
613+
614+
session=$(jq -r .result.output.SECRET <<<"$output")
615+
[ -n "$session" ]
616+
617+
run govc session.login -X POST /api <<EOF
618+
{
619+
"jsonrpc": "2.0",
620+
"method": "invoke",
621+
"params": {
622+
"serviceId": "com.vmware.cis.session",
623+
"operationId": "delete"
624+
},
625+
"id": "123"
626+
}
627+
EOF
628+
assert_success
629+
630+
id=$(jq -r .id <<<"$output")
631+
assert_equal "$id" "123"
632+
633+
run govc session.login -r -X GET /rest/vcenter/certificate-authority/get-root
634+
assert_success
635+
636+
run jq -r .value <<<"$output"
637+
assert_success
638+
639+
run openssl x509 -text <<<"$output"
640+
assert_success
641+
642+
run govc host.cert.csr -host DC0_H0
643+
assert_success
644+
csr="${output//$'\n'/\\n}"
645+
646+
run govc session.login -r -X POST /rest/vcenter/certificate-authority/sign-cert <<EOF
647+
{ "csr": "$csr", "duration": 30}
648+
EOF
649+
assert_success
650+
651+
run jq -r .value <<<"$output"
652+
assert_success
653+
654+
cert="$output"
655+
run openssl x509 -text <<<"$cert"
656+
assert_success
657+
assert_matches DNS:DC0_H0
658+
}

lookup/simulator/registration_info.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,7 @@ func registrationInfo(ctx *simulator.Context) []types.LookupServiceRegistrationI
3333
}
3434
}
3535

36-
trust := []string{""}
37-
if sm.TLSCert != nil {
38-
trust[0] = sm.TLSCert()
39-
}
36+
trust := []string{sm.TLSCert()}
4037
sdk := opts["vcsim.server.url"] + vim25.Path
4138
admin := opts["config.vpxd.sso.default.admin"]
4239
owner := opts["config.vpxd.sso.solutionUser.name"]

simulator/session_manager.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ package simulator
66

77
import (
88
"context"
9+
"crypto/tls"
10+
"encoding/base64"
911
"fmt"
1012
"net/http"
1113
"net/url"
@@ -30,7 +32,7 @@ type SessionManager struct {
3032
nopLocker
3133

3234
ServiceHostName string
33-
TLSCert func() string
35+
TLS func() *tls.Config
3436
ValidLogin func(*types.Login) bool
3537

3638
sessions map[string]Session
@@ -122,6 +124,13 @@ func (s *SessionManager) Authenticate(u url.URL, req *types.Login) bool {
122124
return req.UserName == u.User.Username() && req.Password == pass
123125
}
124126

127+
func (s *SessionManager) TLSCert() string {
128+
if s.TLS == nil {
129+
return ""
130+
}
131+
return base64.StdEncoding.EncodeToString(s.TLS().Certificates[0].Certificate[0])
132+
}
133+
125134
func (s *SessionManager) Login(ctx *Context, req *types.Login) soap.HasFault {
126135
body := new(methods.LoginBody)
127136

simulator/simulator.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"context"
1010
"crypto/tls"
1111
"crypto/x509"
12-
"encoding/base64"
1312
"encoding/json"
1413
"encoding/pem"
1514
"fmt"
@@ -777,9 +776,7 @@ func (s *Service) NewServer() *Server {
777776
if s.TLS != nil {
778777
ts.TLS = s.TLS
779778
ts.TLS.ClientAuth = tls.RequestClientCert // Used by SessionManager.LoginExtensionByCertificate
780-
ctx.Map.SessionManager().TLSCert = func() string {
781-
return base64.StdEncoding.EncodeToString(ts.TLS.Certificates[0].Certificate[0])
782-
}
779+
ctx.Map.SessionManager().TLS = func() *tls.Config { return ts.TLS }
783780
ts.StartTLS()
784781
} else {
785782
ts.Start()

ssoadmin/simulator/simulator.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ func (*ConfigurationManagementService) GetTrustedCertificates(ctx *simulator.Con
270270
}
271271
res = append(res, base64.StdEncoding.EncodeToString(block.Bytes))
272272
}
273-
} else if m.TLSCert != nil {
273+
} else if m.TLS != nil {
274274
res = append(res, m.TLSCert())
275275
}
276276

vapi/simulator/simulator.go

Lines changed: 110 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"bytes"
1010
"context"
1111
"crypto/md5"
12+
"crypto/rand"
1213
"crypto/sha1"
1314
"crypto/sha256"
1415
"crypto/sha512"
@@ -22,6 +23,7 @@ import (
2223
"hash"
2324
"io"
2425
"log"
26+
"math/big"
2527
"net/http"
2628
"net/url"
2729
"os"
@@ -167,8 +169,10 @@ func New(u *url.URL, r *simulator.Registry) ([]string, http.Handler) {
167169
{internal.VCenterOVFLibraryItem + "/", s.libraryItemOVFID},
168170
{internal.VCenterVMTXLibraryItem, s.libraryItemCreateTemplate},
169171
{internal.VCenterVMTXLibraryItem + "/", s.libraryItemTemplateID},
172+
{"/vcenter/certificate-authority/", s.certificateAuthority},
170173
{internal.DebugEcho, s.debugEcho},
171174
// /api/ patterns.
175+
{vapi.Path, s.jsonRPC},
172176
{internal.SecurityPoliciesPath, s.librarySecurityPolicies},
173177
{internal.TrustedCertificatesPath, s.libraryTrustedCertificates},
174178
{internal.TrustedCertificatesPath + "/", s.libraryTrustedCertificatesID},
@@ -179,7 +183,10 @@ func New(u *url.URL, r *simulator.Registry) ([]string, http.Handler) {
179183
s.HandleFunc(h.p, h.m)
180184
}
181185

182-
return []string{rest.Path + "/", vapi.Path + "/"}, s
186+
return []string{
187+
rest.Path, rest.Path + "/",
188+
vapi.Path, vapi.Path + "/",
189+
}, s
183190
}
184191

185192
func (s *handler) withClient(f func(context.Context, *vim25.Client) error) error {
@@ -266,8 +273,13 @@ func (s *handler) HandleFunc(pattern string, handler func(http.ResponseWriter, *
266273
}
267274

268275
func (s *handler) isAuthorized(r *http.Request) bool {
269-
if r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, internal.SessionPath) && s.action(r) == "" {
270-
return true
276+
if r.Method == http.MethodPost && s.action(r) == "" {
277+
if r.URL.Path == vapi.Path {
278+
return true
279+
}
280+
if strings.HasSuffix(r.URL.Path, internal.SessionPath) {
281+
return true
282+
}
271283
}
272284
id := r.Header.Get(internal.SessionCookieName)
273285
if id == "" {
@@ -565,6 +577,101 @@ func (s *handler) session(w http.ResponseWriter, r *http.Request) {
565577
}
566578
}
567579

580+
// just enough json-rpc to support Supervisor upgrade testing
581+
func (s *handler) jsonRPC(w http.ResponseWriter, r *http.Request) {
582+
if r.Method != http.MethodPost {
583+
w.WriteHeader(http.StatusMethodNotAllowed)
584+
return
585+
}
586+
587+
var rpc, out map[string]any
588+
589+
if Decode(r, w, &rpc) {
590+
params := rpc["params"].(map[string]any)
591+
592+
switch params["serviceId"] {
593+
case "com.vmware.cis.session":
594+
switch params["operationId"] {
595+
case "create":
596+
id := uuid.New().String()
597+
now := time.Now()
598+
s.Session[id] = &rest.Session{User: id, Created: now, LastAccessed: now}
599+
out = map[string]any{"SECRET": id}
600+
case "delete":
601+
}
602+
}
603+
604+
res := map[string]any{
605+
"jsonrpc": rpc["jsonrpc"],
606+
"id": rpc["id"],
607+
"result": map[string]any{
608+
"output": out,
609+
},
610+
}
611+
612+
StatusOK(w, res)
613+
}
614+
}
615+
616+
func (s *handler) certificateAuthority(w http.ResponseWriter, r *http.Request) {
617+
signer := s.Map.SessionManager().TLS().Certificates[0]
618+
619+
switch path.Base(r.URL.Path) {
620+
case "get-root":
621+
var encoded bytes.Buffer
622+
_ = pem.Encode(&encoded, &pem.Block{Type: "CERTIFICATE", Bytes: signer.Leaf.Raw})
623+
OK(w, encoded.String())
624+
case "sign-cert":
625+
if r.Method != http.MethodPost {
626+
w.WriteHeader(http.StatusMethodNotAllowed)
627+
return
628+
}
629+
630+
var req struct {
631+
Duration int64 `json:"duration"`
632+
CSR string `json:"csr"`
633+
}
634+
635+
if Decode(r, w, &req) {
636+
block, _ := pem.Decode([]byte(req.CSR))
637+
csr, err := x509.ParseCertificateRequest(block.Bytes)
638+
if err != nil {
639+
BadRequest(w, err.Error())
640+
return
641+
}
642+
643+
serialNumber, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
644+
now := time.Now()
645+
cert := &x509.Certificate{
646+
SerialNumber: serialNumber,
647+
Subject: csr.Subject,
648+
DNSNames: csr.DNSNames,
649+
IPAddresses: csr.IPAddresses,
650+
NotBefore: now,
651+
NotAfter: now.Add(time.Hour * 24 * time.Duration(req.Duration)),
652+
AuthorityKeyId: signer.Leaf.SubjectKeyId,
653+
}
654+
655+
der, err := x509.CreateCertificate(rand.Reader, cert, signer.Leaf, csr.PublicKey, signer.PrivateKey)
656+
if err != nil {
657+
BadRequest(w, err.Error())
658+
return
659+
}
660+
661+
var encoded bytes.Buffer
662+
err = pem.Encode(&encoded, &pem.Block{Type: "CERTIFICATE", Bytes: der})
663+
if err != nil {
664+
BadRequest(w, err.Error())
665+
return
666+
}
667+
668+
OK(w, encoded.String())
669+
}
670+
default:
671+
http.NotFound(w, r)
672+
}
673+
}
674+
568675
func (s *handler) action(r *http.Request) string {
569676
return r.URL.Query().Get("~action")
570677
}

0 commit comments

Comments
 (0)