921 lines
23 KiB
Go
921 lines
23 KiB
Go
|
/*
|
||
|
Copyright (c) 2017-2018 VMware, Inc. All Rights Reserved.
|
||
|
|
||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
you may not use this file except in compliance with the License.
|
||
|
You may obtain a copy of the License at
|
||
|
|
||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||
|
|
||
|
Unless required by applicable law or agreed to in writing, software
|
||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
See the License for the specific language governing permissions and
|
||
|
limitations under the License.
|
||
|
*/
|
||
|
|
||
|
package simulator
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"context"
|
||
|
"crypto/tls"
|
||
|
"crypto/x509"
|
||
|
"encoding/base64"
|
||
|
"encoding/json"
|
||
|
"encoding/pem"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"io/ioutil"
|
||
|
"log"
|
||
|
"math/rand"
|
||
|
"net"
|
||
|
"net/http"
|
||
|
"net/url"
|
||
|
"os"
|
||
|
"path"
|
||
|
"reflect"
|
||
|
"sort"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"time"
|
||
|
|
||
|
"github.com/vmware/govmomi/find"
|
||
|
"github.com/vmware/govmomi/object"
|
||
|
"github.com/vmware/govmomi/simulator/internal"
|
||
|
"github.com/vmware/govmomi/vim25"
|
||
|
"github.com/vmware/govmomi/vim25/mo"
|
||
|
"github.com/vmware/govmomi/vim25/soap"
|
||
|
"github.com/vmware/govmomi/vim25/types"
|
||
|
"github.com/vmware/govmomi/vim25/xml"
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
// Trace when set to true, writes SOAP traffic to stderr
|
||
|
Trace = false
|
||
|
|
||
|
// TraceFile is the output file when Trace = true
|
||
|
TraceFile = os.Stderr
|
||
|
|
||
|
// DefaultLogin for authentication
|
||
|
DefaultLogin = url.UserPassword("user", "pass")
|
||
|
)
|
||
|
|
||
|
// Method encapsulates a decoded SOAP client request
|
||
|
type Method struct {
|
||
|
Name string
|
||
|
This types.ManagedObjectReference
|
||
|
Header soap.Header
|
||
|
Body types.AnyType
|
||
|
}
|
||
|
|
||
|
// Service decodes incoming requests and dispatches to a Handler
|
||
|
type Service struct {
|
||
|
client *vim25.Client
|
||
|
sm *SessionManager
|
||
|
sdk map[string]*Registry
|
||
|
funcs []handleFunc
|
||
|
delay *DelayConfig
|
||
|
|
||
|
readAll func(io.Reader) ([]byte, error)
|
||
|
|
||
|
Listen *url.URL
|
||
|
TLS *tls.Config
|
||
|
ServeMux *http.ServeMux
|
||
|
// RegisterEndpoints will initialize any endpoints added via RegisterEndpoint
|
||
|
RegisterEndpoints bool
|
||
|
}
|
||
|
|
||
|
// Server provides a simulator Service over HTTP
|
||
|
type Server struct {
|
||
|
*internal.Server
|
||
|
URL *url.URL
|
||
|
Tunnel int
|
||
|
|
||
|
caFile string
|
||
|
}
|
||
|
|
||
|
// New returns an initialized simulator Service instance
|
||
|
func New(instance *ServiceInstance) *Service {
|
||
|
s := &Service{
|
||
|
readAll: ioutil.ReadAll,
|
||
|
sm: Map.SessionManager(),
|
||
|
sdk: make(map[string]*Registry),
|
||
|
}
|
||
|
|
||
|
s.client, _ = vim25.NewClient(context.Background(), s)
|
||
|
|
||
|
return s
|
||
|
}
|
||
|
|
||
|
type serverFaultBody struct {
|
||
|
Reason *soap.Fault `xml:"http://schemas.xmlsoap.org/soap/envelope/ Fault,omitempty"`
|
||
|
}
|
||
|
|
||
|
func (b *serverFaultBody) Fault() *soap.Fault { return b.Reason }
|
||
|
|
||
|
func serverFault(msg string) soap.HasFault {
|
||
|
return &serverFaultBody{Reason: Fault(msg, &types.InvalidRequest{})}
|
||
|
}
|
||
|
|
||
|
// Fault wraps the given message and fault in a soap.Fault
|
||
|
func Fault(msg string, fault types.BaseMethodFault) *soap.Fault {
|
||
|
f := &soap.Fault{
|
||
|
Code: "ServerFaultCode",
|
||
|
String: msg,
|
||
|
}
|
||
|
|
||
|
f.Detail.Fault = fault
|
||
|
|
||
|
return f
|
||
|
}
|
||
|
|
||
|
func (s *Service) call(ctx *Context, method *Method) soap.HasFault {
|
||
|
handler := ctx.Map.Get(method.This)
|
||
|
session := ctx.Session
|
||
|
ctx.Caller = &method.This
|
||
|
|
||
|
if session == nil {
|
||
|
switch method.Name {
|
||
|
case "RetrieveServiceContent", "PbmRetrieveServiceContent", "Fetch", "List", "Login", "LoginByToken", "LoginExtensionByCertificate", "RetrieveProperties", "RetrievePropertiesEx", "CloneSession":
|
||
|
// ok for now, TODO: authz
|
||
|
default:
|
||
|
fault := &types.NotAuthenticated{
|
||
|
NoPermission: types.NoPermission{
|
||
|
Object: method.This,
|
||
|
PrivilegeId: "System.View",
|
||
|
},
|
||
|
}
|
||
|
return &serverFaultBody{Reason: Fault("", fault)}
|
||
|
}
|
||
|
} else {
|
||
|
// Prefer the Session.Registry, ServiceContent.PropertyCollector filter field for example is per-session
|
||
|
if h := session.Get(method.This); h != nil {
|
||
|
handler = h
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if handler == nil {
|
||
|
msg := fmt.Sprintf("managed object not found: %s", method.This)
|
||
|
log.Print(msg)
|
||
|
fault := &types.ManagedObjectNotFound{Obj: method.This}
|
||
|
return &serverFaultBody{Reason: Fault(msg, fault)}
|
||
|
}
|
||
|
|
||
|
// Lowercase methods can't be accessed outside their package
|
||
|
name := strings.Title(method.Name)
|
||
|
|
||
|
if strings.HasSuffix(name, vTaskSuffix) {
|
||
|
// Make golint happy renaming "Foo_Task" -> "FooTask"
|
||
|
name = name[:len(name)-len(vTaskSuffix)] + sTaskSuffix
|
||
|
}
|
||
|
|
||
|
m := reflect.ValueOf(handler).MethodByName(name)
|
||
|
if !m.IsValid() {
|
||
|
msg := fmt.Sprintf("%s does not implement: %s", method.This, method.Name)
|
||
|
log.Print(msg)
|
||
|
fault := &types.MethodNotFound{Receiver: method.This, Method: method.Name}
|
||
|
return &serverFaultBody{Reason: Fault(msg, fault)}
|
||
|
}
|
||
|
|
||
|
if e, ok := handler.(mo.Entity); ok {
|
||
|
for _, dm := range e.Entity().DisabledMethod {
|
||
|
if name == dm {
|
||
|
msg := fmt.Sprintf("%s method is disabled: %s", method.This, method.Name)
|
||
|
fault := &types.MethodDisabled{}
|
||
|
return &serverFaultBody{Reason: Fault(msg, fault)}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// We have a valid call. Introduce a delay if requested
|
||
|
//
|
||
|
if s.delay != nil {
|
||
|
d := 0
|
||
|
if s.delay.Delay > 0 {
|
||
|
d = s.delay.Delay
|
||
|
}
|
||
|
if md, ok := s.delay.MethodDelay[method.Name]; ok {
|
||
|
d += md
|
||
|
}
|
||
|
if s.delay.DelayJitter > 0 {
|
||
|
d += int(rand.NormFloat64() * s.delay.DelayJitter * float64(d))
|
||
|
}
|
||
|
if d > 0 {
|
||
|
//fmt.Printf("Delaying method %s %d ms\n", name, d)
|
||
|
time.Sleep(time.Duration(d) * time.Millisecond)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var args, res []reflect.Value
|
||
|
if m.Type().NumIn() == 2 {
|
||
|
args = append(args, reflect.ValueOf(ctx))
|
||
|
}
|
||
|
args = append(args, reflect.ValueOf(method.Body))
|
||
|
ctx.Map.WithLock(handler, func() {
|
||
|
res = m.Call(args)
|
||
|
})
|
||
|
|
||
|
return res[0].Interface().(soap.HasFault)
|
||
|
}
|
||
|
|
||
|
// RoundTrip implements the soap.RoundTripper interface in process.
|
||
|
// Rather than encode/decode SOAP over HTTP, this implementation uses reflection.
|
||
|
func (s *Service) RoundTrip(ctx context.Context, request, response soap.HasFault) error {
|
||
|
field := func(r soap.HasFault, name string) reflect.Value {
|
||
|
return reflect.ValueOf(r).Elem().FieldByName(name)
|
||
|
}
|
||
|
|
||
|
// Every struct passed to soap.RoundTrip has "Req" and "Res" fields
|
||
|
req := field(request, "Req")
|
||
|
|
||
|
// Every request has a "This" field.
|
||
|
this := req.Elem().FieldByName("This")
|
||
|
|
||
|
method := &Method{
|
||
|
Name: req.Elem().Type().Name(),
|
||
|
This: this.Interface().(types.ManagedObjectReference),
|
||
|
Body: req.Interface(),
|
||
|
}
|
||
|
|
||
|
res := s.call(&Context{
|
||
|
Map: Map,
|
||
|
Context: ctx,
|
||
|
Session: internalContext.Session,
|
||
|
}, method)
|
||
|
|
||
|
if err := res.Fault(); err != nil {
|
||
|
return soap.WrapSoapFault(err)
|
||
|
}
|
||
|
|
||
|
field(response, "Res").Set(field(res, "Res"))
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// soapEnvelope is a copy of soap.Envelope, with namespace changed to "soapenv",
|
||
|
// and additional namespace attributes required by some client libraries.
|
||
|
// Go still has issues decoding with such a namespace, but encoding is ok.
|
||
|
type soapEnvelope struct {
|
||
|
XMLName xml.Name `xml:"soapenv:Envelope"`
|
||
|
Enc string `xml:"xmlns:soapenc,attr"`
|
||
|
Env string `xml:"xmlns:soapenv,attr"`
|
||
|
XSD string `xml:"xmlns:xsd,attr"`
|
||
|
XSI string `xml:"xmlns:xsi,attr"`
|
||
|
Body interface{} `xml:"soapenv:Body"`
|
||
|
}
|
||
|
|
||
|
type faultDetail struct {
|
||
|
Fault types.AnyType
|
||
|
}
|
||
|
|
||
|
// soapFault is a copy of soap.Fault, with the same changes as soapEnvelope
|
||
|
type soapFault struct {
|
||
|
XMLName xml.Name `xml:"soapenv:Fault"`
|
||
|
Code string `xml:"faultcode"`
|
||
|
String string `xml:"faultstring"`
|
||
|
Detail struct {
|
||
|
Fault *faultDetail
|
||
|
} `xml:"detail"`
|
||
|
}
|
||
|
|
||
|
// MarshalXML renames the start element from "Fault" to "${Type}Fault"
|
||
|
func (d *faultDetail) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||
|
kind := reflect.TypeOf(d.Fault).Elem().Name()
|
||
|
start.Name.Local = kind + "Fault"
|
||
|
start.Attr = append(start.Attr,
|
||
|
xml.Attr{
|
||
|
Name: xml.Name{Local: "xmlns"},
|
||
|
Value: "urn:" + vim25.Namespace,
|
||
|
},
|
||
|
xml.Attr{
|
||
|
Name: xml.Name{Local: "xsi:type"},
|
||
|
Value: kind,
|
||
|
})
|
||
|
return e.EncodeElement(d.Fault, start)
|
||
|
}
|
||
|
|
||
|
// response sets xml.Name.Space when encoding Body.
|
||
|
// Note that namespace is intentionally omitted in the vim25/methods/methods.go Body.Res field tags.
|
||
|
type response struct {
|
||
|
Namespace string
|
||
|
Body soap.HasFault
|
||
|
}
|
||
|
|
||
|
func (r *response) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||
|
val := reflect.ValueOf(r.Body).Elem().FieldByName("Res")
|
||
|
if !val.IsValid() {
|
||
|
return fmt.Errorf("%T: invalid response type (missing 'Res' field)", r.Body)
|
||
|
}
|
||
|
if val.IsNil() {
|
||
|
return fmt.Errorf("%T: invalid response (nil 'Res' field)", r.Body)
|
||
|
}
|
||
|
res := xml.StartElement{
|
||
|
Name: xml.Name{
|
||
|
Space: "urn:" + r.Namespace,
|
||
|
Local: val.Elem().Type().Name(),
|
||
|
},
|
||
|
}
|
||
|
if err := e.EncodeToken(start); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
if err := e.EncodeElement(val.Interface(), res); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return e.EncodeToken(start.End())
|
||
|
}
|
||
|
|
||
|
// About generates some info about the simulator.
|
||
|
func (s *Service) About(w http.ResponseWriter, r *http.Request) {
|
||
|
var about struct {
|
||
|
Methods []string
|
||
|
Types []string
|
||
|
}
|
||
|
|
||
|
seen := make(map[string]bool)
|
||
|
|
||
|
f := reflect.TypeOf((*soap.HasFault)(nil)).Elem()
|
||
|
|
||
|
for _, obj := range Map.objects {
|
||
|
kind := obj.Reference().Type
|
||
|
if seen[kind] {
|
||
|
continue
|
||
|
}
|
||
|
seen[kind] = true
|
||
|
|
||
|
about.Types = append(about.Types, kind)
|
||
|
|
||
|
t := reflect.TypeOf(obj)
|
||
|
for i := 0; i < t.NumMethod(); i++ {
|
||
|
m := t.Method(i)
|
||
|
if seen[m.Name] {
|
||
|
continue
|
||
|
}
|
||
|
seen[m.Name] = true
|
||
|
|
||
|
in := m.Type.NumIn()
|
||
|
if in < 2 || in > 3 { // at least 2 params (receiver and request), optionally a 3rd param (context)
|
||
|
continue
|
||
|
}
|
||
|
if m.Type.NumOut() != 1 || m.Type.Out(0) != f { // all methods return soap.HasFault
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
about.Methods = append(about.Methods, strings.Replace(m.Name, "Task", "_Task", 1))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
sort.Strings(about.Methods)
|
||
|
sort.Strings(about.Types)
|
||
|
|
||
|
w.Header().Set("Content-Type", "application/json")
|
||
|
enc := json.NewEncoder(w)
|
||
|
enc.SetIndent("", " ")
|
||
|
_ = enc.Encode(&about)
|
||
|
}
|
||
|
|
||
|
var endpoints []func(*Service, *Registry)
|
||
|
|
||
|
// RegisterEndpoint funcs are called after the Server is initialized if Service.RegisterEndpoints=true.
|
||
|
// Such a func would typically register a SOAP endpoint via Service.RegisterSDK or REST endpoint via Service.Handle
|
||
|
func RegisterEndpoint(endpoint func(*Service, *Registry)) {
|
||
|
endpoints = append(endpoints, endpoint)
|
||
|
}
|
||
|
|
||
|
// Handle registers the handler for the given pattern with Service.ServeMux.
|
||
|
func (s *Service) Handle(pattern string, handler http.Handler) {
|
||
|
s.ServeMux.Handle(pattern, handler)
|
||
|
// Not ideal, but avoids having to add yet another registration mechanism
|
||
|
// so we can optionally use vapi/simulator internally.
|
||
|
if m, ok := handler.(tagManager); ok {
|
||
|
s.sdk[vim25.Path].tagManager = m
|
||
|
}
|
||
|
}
|
||
|
|
||
|
type muxHandleFunc interface {
|
||
|
HandleFunc(string, func(http.ResponseWriter, *http.Request))
|
||
|
}
|
||
|
|
||
|
type handleFunc struct {
|
||
|
pattern string
|
||
|
handler func(http.ResponseWriter, *http.Request)
|
||
|
}
|
||
|
|
||
|
// HandleFunc dispatches to http.ServeMux.HandleFunc after all endpoints have been registered.
|
||
|
// This allows dispatching to an endpoint's HandleFunc impl, such as vapi/simulator for example.
|
||
|
func (s *Service) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) {
|
||
|
s.funcs = append(s.funcs, handleFunc{pattern, handler})
|
||
|
}
|
||
|
|
||
|
// RegisterSDK adds an HTTP handler for the Registry's Path and Namespace.
|
||
|
func (s *Service) RegisterSDK(r *Registry) {
|
||
|
if s.ServeMux == nil {
|
||
|
s.ServeMux = http.NewServeMux()
|
||
|
}
|
||
|
|
||
|
s.sdk[r.Path] = r
|
||
|
s.ServeMux.HandleFunc(r.Path, s.ServeSDK)
|
||
|
}
|
||
|
|
||
|
// StatusSDK can be used to simulate an /sdk HTTP response code other than 200.
|
||
|
// The value of StatusSDK is restored to http.StatusOK after 1 response.
|
||
|
// This can be useful to test vim25.Retry() for example.
|
||
|
var StatusSDK = http.StatusOK
|
||
|
|
||
|
// ServeSDK implements the http.Handler interface
|
||
|
func (s *Service) ServeSDK(w http.ResponseWriter, r *http.Request) {
|
||
|
if r.Method != http.MethodPost {
|
||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if StatusSDK != http.StatusOK {
|
||
|
w.WriteHeader(StatusSDK)
|
||
|
StatusSDK = http.StatusOK // reset
|
||
|
return
|
||
|
}
|
||
|
|
||
|
body, err := s.readAll(r.Body)
|
||
|
_ = r.Body.Close()
|
||
|
if err != nil {
|
||
|
log.Printf("error reading body: %s", err)
|
||
|
w.WriteHeader(http.StatusBadRequest)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if Trace {
|
||
|
fmt.Fprintf(TraceFile, "Request: %s\n", string(body))
|
||
|
}
|
||
|
|
||
|
ctx := &Context{
|
||
|
req: r,
|
||
|
res: w,
|
||
|
svc: s,
|
||
|
|
||
|
Map: s.sdk[r.URL.Path],
|
||
|
Context: context.Background(),
|
||
|
}
|
||
|
ctx.Map.WithLock(s.sm, ctx.mapSession)
|
||
|
|
||
|
var res soap.HasFault
|
||
|
var soapBody interface{}
|
||
|
|
||
|
method, err := UnmarshalBody(ctx.Map.typeFunc, body)
|
||
|
if err != nil {
|
||
|
res = serverFault(err.Error())
|
||
|
} else {
|
||
|
ctx.Header = method.Header
|
||
|
if method.Name == "Fetch" {
|
||
|
// Redirect any Fetch method calls to the PropertyCollector singleton
|
||
|
method.This = ctx.Map.content().PropertyCollector
|
||
|
}
|
||
|
res = s.call(ctx, method)
|
||
|
}
|
||
|
|
||
|
if f := res.Fault(); f != nil {
|
||
|
w.WriteHeader(http.StatusInternalServerError)
|
||
|
|
||
|
// the generated method/*Body structs use the '*soap.Fault' type,
|
||
|
// so we need our own Body type to use the modified '*soapFault' type.
|
||
|
soapBody = struct {
|
||
|
Fault *soapFault
|
||
|
}{
|
||
|
&soapFault{
|
||
|
Code: f.Code,
|
||
|
String: f.String,
|
||
|
Detail: struct {
|
||
|
Fault *faultDetail
|
||
|
}{&faultDetail{f.Detail.Fault}},
|
||
|
},
|
||
|
}
|
||
|
} else {
|
||
|
w.WriteHeader(http.StatusOK)
|
||
|
|
||
|
soapBody = &response{ctx.Map.Namespace, res}
|
||
|
}
|
||
|
|
||
|
var out bytes.Buffer
|
||
|
|
||
|
fmt.Fprint(&out, xml.Header)
|
||
|
e := xml.NewEncoder(&out)
|
||
|
err = e.Encode(&soapEnvelope{
|
||
|
Enc: "http://schemas.xmlsoap.org/soap/encoding/",
|
||
|
Env: "http://schemas.xmlsoap.org/soap/envelope/",
|
||
|
XSD: "http://www.w3.org/2001/XMLSchema",
|
||
|
XSI: "http://www.w3.org/2001/XMLSchema-instance",
|
||
|
Body: soapBody,
|
||
|
})
|
||
|
if err == nil {
|
||
|
err = e.Flush()
|
||
|
}
|
||
|
|
||
|
if err != nil {
|
||
|
log.Printf("error encoding %s response: %s", method.Name, err)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if Trace {
|
||
|
fmt.Fprintf(TraceFile, "Response: %s\n", out.String())
|
||
|
}
|
||
|
|
||
|
_, _ = w.Write(out.Bytes())
|
||
|
}
|
||
|
|
||
|
func (s *Service) findDatastore(query url.Values) (*Datastore, error) {
|
||
|
ctx := context.Background()
|
||
|
|
||
|
finder := find.NewFinder(s.client, false)
|
||
|
dc, err := finder.DatacenterOrDefault(ctx, query.Get("dcPath"))
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
finder.SetDatacenter(dc)
|
||
|
|
||
|
ds, err := finder.DatastoreOrDefault(ctx, query.Get("dsName"))
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return Map.Get(ds.Reference()).(*Datastore), nil
|
||
|
}
|
||
|
|
||
|
const folderPrefix = "/folder/"
|
||
|
|
||
|
// ServeDatastore handler for Datastore access via /folder path.
|
||
|
func (s *Service) ServeDatastore(w http.ResponseWriter, r *http.Request) {
|
||
|
ds, ferr := s.findDatastore(r.URL.Query())
|
||
|
if ferr != nil {
|
||
|
log.Printf("failed to locate datastore with query params: %s", r.URL.RawQuery)
|
||
|
w.WriteHeader(http.StatusNotFound)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
r.URL.Path = strings.TrimPrefix(r.URL.Path, folderPrefix)
|
||
|
p := path.Join(ds.Info.GetDatastoreInfo().Url, r.URL.Path)
|
||
|
|
||
|
switch r.Method {
|
||
|
case http.MethodPost:
|
||
|
_, err := os.Stat(p)
|
||
|
if err == nil {
|
||
|
// File exists
|
||
|
w.WriteHeader(http.StatusConflict)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// File does not exist, fallthrough to create via PUT logic
|
||
|
fallthrough
|
||
|
case http.MethodPut:
|
||
|
dir := path.Dir(p)
|
||
|
_ = os.MkdirAll(dir, 0700)
|
||
|
|
||
|
f, err := os.Create(p)
|
||
|
if err != nil {
|
||
|
log.Printf("failed to %s '%s': %s", r.Method, p, err)
|
||
|
w.WriteHeader(http.StatusInternalServerError)
|
||
|
return
|
||
|
}
|
||
|
defer f.Close()
|
||
|
|
||
|
_, _ = io.Copy(f, r.Body)
|
||
|
default:
|
||
|
fs := http.FileServer(http.Dir(ds.Info.GetDatastoreInfo().Url))
|
||
|
|
||
|
fs.ServeHTTP(w, r)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// ServiceVersions handler for the /sdk/vimServiceVersions.xml path.
|
||
|
func (s *Service) ServiceVersions(w http.ResponseWriter, r *http.Request) {
|
||
|
const versions = xml.Header + `<namespaces version="1.0">
|
||
|
<namespace>
|
||
|
<name>urn:vim25</name>
|
||
|
<version>%s</version>
|
||
|
<priorVersions>
|
||
|
<version>6.0</version>
|
||
|
<version>5.5</version>
|
||
|
</priorVersions>
|
||
|
</namespace>
|
||
|
</namespaces>
|
||
|
`
|
||
|
fmt.Fprintf(w, versions, s.client.ServiceContent.About.ApiVersion)
|
||
|
}
|
||
|
|
||
|
// defaultIP returns addr.IP if specified, otherwise attempts to find a non-loopback ipv4 IP
|
||
|
func defaultIP(addr *net.TCPAddr) string {
|
||
|
if !addr.IP.IsUnspecified() {
|
||
|
return addr.IP.String()
|
||
|
}
|
||
|
|
||
|
nics, err := net.Interfaces()
|
||
|
if err != nil {
|
||
|
return addr.IP.String()
|
||
|
}
|
||
|
|
||
|
for _, nic := range nics {
|
||
|
if nic.Name == "docker0" || strings.HasPrefix(nic.Name, "vmnet") {
|
||
|
continue
|
||
|
}
|
||
|
addrs, aerr := nic.Addrs()
|
||
|
if aerr != nil {
|
||
|
continue
|
||
|
}
|
||
|
for _, addr := range addrs {
|
||
|
if ip, ok := addr.(*net.IPNet); ok && !ip.IP.IsLoopback() {
|
||
|
if ip.IP.To4() != nil {
|
||
|
return ip.IP.String()
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return addr.IP.String()
|
||
|
}
|
||
|
|
||
|
// NewServer returns an http Server instance for the given service
|
||
|
func (s *Service) NewServer() *Server {
|
||
|
s.RegisterSDK(Map)
|
||
|
|
||
|
mux := s.ServeMux
|
||
|
vim := Map.Path + "/vimService"
|
||
|
s.sdk[vim] = s.sdk[vim25.Path]
|
||
|
mux.HandleFunc(vim, s.ServeSDK)
|
||
|
mux.HandleFunc(Map.Path+"/vimServiceVersions.xml", s.ServiceVersions)
|
||
|
mux.HandleFunc(folderPrefix, s.ServeDatastore)
|
||
|
mux.HandleFunc(guestPrefix, ServeGuest)
|
||
|
mux.HandleFunc(nfcPrefix, ServeNFC)
|
||
|
mux.HandleFunc("/about", s.About)
|
||
|
|
||
|
if s.Listen == nil {
|
||
|
s.Listen = new(url.URL)
|
||
|
}
|
||
|
ts := internal.NewUnstartedServer(mux, s.Listen.Host)
|
||
|
addr := ts.Listener.Addr().(*net.TCPAddr)
|
||
|
port := strconv.Itoa(addr.Port)
|
||
|
u := &url.URL{
|
||
|
Scheme: "http",
|
||
|
Host: net.JoinHostPort(defaultIP(addr), port),
|
||
|
Path: Map.Path,
|
||
|
}
|
||
|
if s.TLS != nil {
|
||
|
u.Scheme += "s"
|
||
|
}
|
||
|
|
||
|
// Redirect clients to this http server, rather than HostSystem.Name
|
||
|
Map.SessionManager().ServiceHostName = u.Host
|
||
|
|
||
|
// Add vcsim config to OptionManager for use by SDK handlers (see lookup/simulator for example)
|
||
|
m := Map.OptionManager()
|
||
|
for i := range m.Setting {
|
||
|
setting := m.Setting[i].GetOptionValue()
|
||
|
|
||
|
if strings.HasSuffix(setting.Key, ".uri") {
|
||
|
// Rewrite any URIs with vcsim's host:port
|
||
|
endpoint, err := url.Parse(setting.Value.(string))
|
||
|
if err == nil {
|
||
|
endpoint.Scheme = u.Scheme
|
||
|
endpoint.Host = u.Host
|
||
|
setting.Value = endpoint.String()
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
m.Setting = append(m.Setting,
|
||
|
&types.OptionValue{
|
||
|
Key: "vcsim.server.url",
|
||
|
Value: u.String(),
|
||
|
},
|
||
|
)
|
||
|
|
||
|
u.User = s.Listen.User
|
||
|
if u.User == nil {
|
||
|
u.User = DefaultLogin
|
||
|
}
|
||
|
s.Listen = u
|
||
|
|
||
|
if s.RegisterEndpoints {
|
||
|
for i := range endpoints {
|
||
|
endpoints[i](s, Map)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for _, f := range s.funcs {
|
||
|
pattern := &url.URL{Path: f.pattern}
|
||
|
endpoint, _ := s.ServeMux.Handler(&http.Request{URL: pattern})
|
||
|
|
||
|
if mux, ok := endpoint.(muxHandleFunc); ok {
|
||
|
mux.HandleFunc(f.pattern, f.handler) // e.g. vapi/simulator
|
||
|
} else {
|
||
|
s.ServeMux.HandleFunc(f.pattern, f.handler)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if s.TLS != nil {
|
||
|
ts.TLS = s.TLS
|
||
|
ts.TLS.ClientAuth = tls.RequestClientCert // Used by SessionManager.LoginExtensionByCertificate
|
||
|
Map.SessionManager().TLSCert = func() string {
|
||
|
return base64.StdEncoding.EncodeToString(ts.TLS.Certificates[0].Certificate[0])
|
||
|
}
|
||
|
ts.StartTLS()
|
||
|
} else {
|
||
|
ts.Start()
|
||
|
}
|
||
|
|
||
|
return &Server{
|
||
|
Server: ts,
|
||
|
URL: u,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Certificate returns the TLS certificate for the Server if started with TLS enabled.
|
||
|
// This method will panic if TLS is not enabled for the server.
|
||
|
func (s *Server) Certificate() *x509.Certificate {
|
||
|
// By default httptest.StartTLS uses http/internal.LocalhostCert, which we can access here:
|
||
|
cert, _ := x509.ParseCertificate(s.TLS.Certificates[0].Certificate[0])
|
||
|
return cert
|
||
|
}
|
||
|
|
||
|
// CertificateInfo returns Server.Certificate() as object.HostCertificateInfo
|
||
|
func (s *Server) CertificateInfo() *object.HostCertificateInfo {
|
||
|
info := new(object.HostCertificateInfo)
|
||
|
info.FromCertificate(s.Certificate())
|
||
|
return info
|
||
|
}
|
||
|
|
||
|
// CertificateFile returns a file name, where the file contains the PEM encoded Server.Certificate.
|
||
|
// The temporary file is removed when Server.Close() is called.
|
||
|
func (s *Server) CertificateFile() (string, error) {
|
||
|
if s.caFile != "" {
|
||
|
return s.caFile, nil
|
||
|
}
|
||
|
|
||
|
f, err := ioutil.TempFile("", "vcsim-")
|
||
|
if err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
defer f.Close()
|
||
|
|
||
|
s.caFile = f.Name()
|
||
|
cert := s.Certificate()
|
||
|
return s.caFile, pem.Encode(f, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})
|
||
|
}
|
||
|
|
||
|
// proxy tunnels SDK requests
|
||
|
func (s *Server) proxy(w http.ResponseWriter, r *http.Request) {
|
||
|
if r.Method != http.MethodConnect {
|
||
|
http.Error(w, "", http.StatusMethodNotAllowed)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
dst, err := net.Dial("tcp", s.URL.Host)
|
||
|
if err != nil {
|
||
|
http.Error(w, err.Error(), http.StatusBadGateway)
|
||
|
return
|
||
|
}
|
||
|
w.WriteHeader(http.StatusOK)
|
||
|
|
||
|
src, _, err := w.(http.Hijacker).Hijack()
|
||
|
if err != nil {
|
||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
go io.Copy(src, dst)
|
||
|
go func() {
|
||
|
_, _ = io.Copy(dst, src)
|
||
|
_ = dst.Close()
|
||
|
_ = src.Close()
|
||
|
}()
|
||
|
}
|
||
|
|
||
|
// StartTunnel runs an HTTP proxy for tunneling SDK requests that require TLS client certificate authentication.
|
||
|
func (s *Server) StartTunnel() error {
|
||
|
tunnel := &http.Server{
|
||
|
Addr: fmt.Sprintf("%s:%d", s.URL.Hostname(), s.Tunnel),
|
||
|
Handler: http.HandlerFunc(s.proxy),
|
||
|
}
|
||
|
|
||
|
l, err := net.Listen("tcp", tunnel.Addr)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
if s.Tunnel == 0 {
|
||
|
s.Tunnel = l.Addr().(*net.TCPAddr).Port
|
||
|
}
|
||
|
|
||
|
// Set client proxy port (defaults to vCenter host port 80 in real life)
|
||
|
q := s.URL.Query()
|
||
|
q.Set("GOVMOMI_TUNNEL_PROXY_PORT", strconv.Itoa(s.Tunnel))
|
||
|
s.URL.RawQuery = q.Encode()
|
||
|
|
||
|
go tunnel.Serve(l)
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Close shuts down the server and blocks until all outstanding
|
||
|
// requests on this server have completed.
|
||
|
func (s *Server) Close() {
|
||
|
s.Server.Close()
|
||
|
if s.caFile != "" {
|
||
|
_ = os.Remove(s.caFile)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var (
|
||
|
vim25MapType = types.TypeFunc()
|
||
|
)
|
||
|
|
||
|
func defaultMapType(name string) (reflect.Type, bool) {
|
||
|
typ, ok := vim25MapType(name)
|
||
|
if !ok {
|
||
|
// See TestIssue945, in which case Go does not resolve the namespace and name == "ns1:TraversalSpec"
|
||
|
// Without this hack, the SelectSet would be all nil's
|
||
|
kind := strings.SplitN(name, ":", 2)
|
||
|
if len(kind) == 2 {
|
||
|
typ, ok = vim25MapType(kind[1])
|
||
|
}
|
||
|
}
|
||
|
return typ, ok
|
||
|
}
|
||
|
|
||
|
// Element can be used to defer decoding of an XML node.
|
||
|
type Element struct {
|
||
|
start xml.StartElement
|
||
|
inner struct {
|
||
|
Content string `xml:",innerxml"`
|
||
|
}
|
||
|
typeFunc func(string) (reflect.Type, bool)
|
||
|
}
|
||
|
|
||
|
func (e *Element) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||
|
e.start = start
|
||
|
|
||
|
return d.DecodeElement(&e.inner, &start)
|
||
|
}
|
||
|
|
||
|
func (e *Element) decoder() *xml.Decoder {
|
||
|
decoder := xml.NewDecoder(strings.NewReader(e.inner.Content))
|
||
|
decoder.TypeFunc = e.typeFunc // required to decode interface types
|
||
|
return decoder
|
||
|
}
|
||
|
|
||
|
func (e *Element) Decode(val interface{}) error {
|
||
|
return e.decoder().DecodeElement(val, &e.start)
|
||
|
}
|
||
|
|
||
|
// UnmarshalBody extracts the Body from a soap.Envelope and unmarshals to the corresponding govmomi type
|
||
|
func UnmarshalBody(typeFunc func(string) (reflect.Type, bool), data []byte) (*Method, error) {
|
||
|
body := &Element{typeFunc: typeFunc}
|
||
|
req := soap.Envelope{
|
||
|
Header: &soap.Header{
|
||
|
Security: new(Element),
|
||
|
},
|
||
|
Body: body,
|
||
|
}
|
||
|
|
||
|
err := xml.Unmarshal(data, &req)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("xml.Unmarshal: %s", err)
|
||
|
}
|
||
|
|
||
|
var start xml.StartElement
|
||
|
var ok bool
|
||
|
decoder := body.decoder()
|
||
|
|
||
|
for {
|
||
|
tok, derr := decoder.Token()
|
||
|
if derr != nil {
|
||
|
return nil, fmt.Errorf("decoding: %s", derr)
|
||
|
}
|
||
|
if start, ok = tok.(xml.StartElement); ok {
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if !ok {
|
||
|
return nil, fmt.Errorf("decoding: method token not found")
|
||
|
}
|
||
|
|
||
|
kind := start.Name.Local
|
||
|
rtype, ok := typeFunc(kind)
|
||
|
if !ok {
|
||
|
return nil, fmt.Errorf("no vmomi type defined for '%s'", kind)
|
||
|
}
|
||
|
|
||
|
val := reflect.New(rtype).Interface()
|
||
|
|
||
|
err = decoder.DecodeElement(val, &start)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("decoding %s: %s", kind, err)
|
||
|
}
|
||
|
|
||
|
method := &Method{Name: kind, Header: *req.Header, Body: val}
|
||
|
|
||
|
field := reflect.ValueOf(val).Elem().FieldByName("This")
|
||
|
|
||
|
method.This = field.Interface().(types.ManagedObjectReference)
|
||
|
|
||
|
return method, nil
|
||
|
}
|