github.com/teknogeek/dnscontrol/v2@v2.10.1-0.20200227202244-ae299b55ba42/pkg/js/js.go (about)

     1  package js
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	"github.com/robertkrimen/otto"              // load underscore js into vm by default
    11  	_ "github.com/robertkrimen/otto/underscore" // required by otto
    12  
    13  	"github.com/StackExchange/dnscontrol/v2/models"
    14  	"github.com/StackExchange/dnscontrol/v2/pkg/printer"
    15  	"github.com/StackExchange/dnscontrol/v2/pkg/transform"
    16  )
    17  
    18  // currentDirectory is the current directory as used by require().
    19  // This is used to emulate nodejs-style require() directory handling.
    20  // If require("a/b/c.js") is called, any require() statement in c.js
    21  // needs to be accessed relative to "a/b".  Therefore we
    22  // track the currentDirectory (which is the current directory as
    23  // far as require() is concerned, not the actual os.Getwd().
    24  var currentDirectory string
    25  
    26  // ExecuteJavascript accepts a javascript string and runs it, returning the resulting dnsConfig.
    27  func ExecuteJavascript(file string, devMode bool) (*models.DNSConfig, error) {
    28  	script, err := ioutil.ReadFile(file)
    29  	if err != nil {
    30  		return nil, fmt.Errorf("Reading js file %s: %s", file, err)
    31  	}
    32  
    33  	// Record the directory path leading up to this file.
    34  	currentDirectory = filepath.Clean(filepath.Dir(file))
    35  
    36  	vm := otto.New()
    37  
    38  	vm.Set("require", require)
    39  	vm.Set("REV", reverse)
    40  
    41  	helperJs := GetHelpers(devMode)
    42  	// run helper script to prime vm and initialize variables
    43  	if _, err := vm.Run(helperJs); err != nil {
    44  		return nil, err
    45  	}
    46  
    47  	// run user script
    48  	if _, err := vm.Run(script); err != nil {
    49  		return nil, err
    50  	}
    51  
    52  	// export conf as string and unmarshal
    53  	value, err := vm.Run(`JSON.stringify(conf)`)
    54  	if err != nil {
    55  		return nil, err
    56  	}
    57  	str, err := value.ToString()
    58  	if err != nil {
    59  		return nil, err
    60  	}
    61  	conf := &models.DNSConfig{}
    62  	if err = json.Unmarshal([]byte(str), conf); err != nil {
    63  		return nil, err
    64  	}
    65  	return conf, nil
    66  }
    67  
    68  // GetHelpers returns the filename of helpers.js, or the esc'ed version.
    69  func GetHelpers(devMode bool) string {
    70  	return _escFSMustString(devMode, "/helpers.js")
    71  }
    72  
    73  func require(call otto.FunctionCall) otto.Value {
    74  	if len(call.ArgumentList) != 1 {
    75  		throw(call.Otto, "require takes exactly one argument")
    76  	}
    77  	file := call.Argument(0).String() // The filename as given by the user
    78  
    79  	// relFile is the file we're actually going to pass to ReadFile().
    80  	// It defaults to the user-provided name unless it is relative.
    81  	relFile := file
    82  	cleanFile := filepath.Clean(filepath.Join(currentDirectory, file))
    83  	if strings.HasPrefix(file, ".") {
    84  		relFile = cleanFile
    85  	}
    86  
    87  	// Record the old currentDirectory so that we can return there.
    88  	currentDirectoryOld := currentDirectory
    89  	// Record the directory path leading up to the file we're about to require.
    90  	currentDirectory = filepath.Clean(filepath.Dir(cleanFile))
    91  
    92  	printer.Debugf("requiring: %s (%s)\n", file, relFile)
    93  	data, err := ioutil.ReadFile(relFile)
    94  
    95  	if err != nil {
    96  		throw(call.Otto, err.Error())
    97  	}
    98  
    99  	var value otto.Value = otto.TrueValue()
   100  
   101  	// If its a json file return the json value, else default to true
   102  	if strings.HasSuffix(filepath.Ext(relFile), "json") {
   103  		cmd := fmt.Sprintf(`JSON.parse(JSON.stringify(%s))`, string(data))
   104  		value, err = call.Otto.Run(cmd)
   105  	} else {
   106  		_, err = call.Otto.Run(string(data))
   107  	}
   108  
   109  	if err != nil {
   110  		throw(call.Otto, err.Error())
   111  	}
   112  
   113  	// Pop back to the old directory.
   114  	currentDirectory = currentDirectoryOld
   115  
   116  	return value
   117  }
   118  
   119  func throw(vm *otto.Otto, str string) {
   120  	panic(vm.MakeCustomError("Error", str))
   121  }
   122  
   123  func reverse(call otto.FunctionCall) otto.Value {
   124  	if len(call.ArgumentList) != 1 {
   125  		throw(call.Otto, "REV takes exactly one argument")
   126  	}
   127  	dom := call.Argument(0).String()
   128  	rev, err := transform.ReverseDomainName(dom)
   129  	if err != nil {
   130  		throw(call.Otto, err.Error())
   131  	}
   132  	v, _ := otto.ToValue(rev)
   133  	return v
   134  }