mirror of
https://github.com/ipfs/kubo.git
synced 2025-06-30 18:13:54 +08:00
Merge pull request #4415 from ipfs/cleanup/dead-cmds-code
Delete some now unused commands lib code
This commit is contained in:
@ -1,89 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
levenshtein "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/texttheater/golang-levenshtein/levenshtein"
|
||||
cmds "github.com/ipfs/go-ipfs/commands"
|
||||
)
|
||||
|
||||
// Make a custom slice that can be sorted by its levenshtein value
|
||||
type suggestionSlice []*suggestion
|
||||
|
||||
type suggestion struct {
|
||||
cmd string
|
||||
levenshtein int
|
||||
}
|
||||
|
||||
func (s suggestionSlice) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
func (s suggestionSlice) Swap(i, j int) {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
|
||||
func (s suggestionSlice) Less(i, j int) bool {
|
||||
return s[i].levenshtein < s[j].levenshtein
|
||||
}
|
||||
|
||||
func suggestUnknownCmd(args []string, root *cmds.Command) []string {
|
||||
if root == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
arg := args[0]
|
||||
var suggestions []string
|
||||
sortableSuggestions := make(suggestionSlice, 0)
|
||||
var sFinal []string
|
||||
const MIN_LEVENSHTEIN = 3
|
||||
|
||||
var options = levenshtein.Options{
|
||||
InsCost: 1,
|
||||
DelCost: 3,
|
||||
SubCost: 2,
|
||||
Matches: func(sourceCharacter rune, targetCharacter rune) bool {
|
||||
return sourceCharacter == targetCharacter
|
||||
},
|
||||
}
|
||||
|
||||
// Start with a simple strings.Contains check
|
||||
for name := range root.Subcommands {
|
||||
if strings.Contains(arg, name) {
|
||||
suggestions = append(suggestions, name)
|
||||
}
|
||||
}
|
||||
|
||||
// If the string compare returns a match, return
|
||||
if len(suggestions) > 0 {
|
||||
return suggestions
|
||||
}
|
||||
|
||||
for name := range root.Subcommands {
|
||||
lev := levenshtein.DistanceForStrings([]rune(arg), []rune(name), options)
|
||||
if lev <= MIN_LEVENSHTEIN {
|
||||
sortableSuggestions = append(sortableSuggestions, &suggestion{name, lev})
|
||||
}
|
||||
}
|
||||
sort.Sort(sortableSuggestions)
|
||||
|
||||
for _, j := range sortableSuggestions {
|
||||
sFinal = append(sFinal, j.cmd)
|
||||
}
|
||||
return sFinal
|
||||
}
|
||||
|
||||
func printSuggestions(inputs []string, root *cmds.Command) (err error) {
|
||||
|
||||
suggestions := suggestUnknownCmd(inputs, root)
|
||||
if len(suggestions) > 1 {
|
||||
err = fmt.Errorf("Unknown Command \"%s\"\n\nDid you mean any of these?\n\n\t%s", inputs[0], strings.Join(suggestions, "\n\t"))
|
||||
} else if len(suggestions) > 0 {
|
||||
err = fmt.Errorf("Unknown Command \"%s\"\n\nDid you mean this?\n\n\t%s", inputs[0], suggestions[0])
|
||||
} else {
|
||||
err = fmt.Errorf("Unknown Command %q", inputs[0])
|
||||
}
|
||||
return
|
||||
}
|
@ -1,449 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
cmds "github.com/ipfs/go-ipfs/commands"
|
||||
cmdkit "gx/ipfs/QmUyfy4QSr3NXym4etEiRyxBLqqAeKHJuRdi8AACxg63fZ/go-ipfs-cmdkit"
|
||||
)
|
||||
|
||||
const (
|
||||
requiredArg = "<%v>"
|
||||
optionalArg = "[<%v>]"
|
||||
variadicArg = "%v..."
|
||||
shortFlag = "-%v"
|
||||
longFlag = "--%v"
|
||||
|
||||
indentStr = " "
|
||||
)
|
||||
|
||||
type helpFields struct {
|
||||
Indent string
|
||||
Usage string
|
||||
Path string
|
||||
ArgUsage string
|
||||
Tagline string
|
||||
Arguments string
|
||||
Options string
|
||||
Synopsis string
|
||||
Subcommands string
|
||||
Description string
|
||||
MoreHelp bool
|
||||
}
|
||||
|
||||
// TrimNewlines removes extra newlines from fields. This makes aligning
|
||||
// commands easier. Below, the leading + tralining newlines are removed:
|
||||
// Synopsis: `
|
||||
// ipfs config <key> - Get value of <key>
|
||||
// ipfs config <key> <value> - Set value of <key> to <value>
|
||||
// ipfs config --show - Show config file
|
||||
// ipfs config --edit - Edit config file in $EDITOR
|
||||
// `
|
||||
func (f *helpFields) TrimNewlines() {
|
||||
f.Path = strings.Trim(f.Path, "\n")
|
||||
f.ArgUsage = strings.Trim(f.ArgUsage, "\n")
|
||||
f.Tagline = strings.Trim(f.Tagline, "\n")
|
||||
f.Arguments = strings.Trim(f.Arguments, "\n")
|
||||
f.Options = strings.Trim(f.Options, "\n")
|
||||
f.Synopsis = strings.Trim(f.Synopsis, "\n")
|
||||
f.Subcommands = strings.Trim(f.Subcommands, "\n")
|
||||
f.Description = strings.Trim(f.Description, "\n")
|
||||
}
|
||||
|
||||
// Indent adds whitespace the lines of fields.
|
||||
func (f *helpFields) IndentAll() {
|
||||
indent := func(s string) string {
|
||||
if s == "" {
|
||||
return s
|
||||
}
|
||||
return indentString(s, indentStr)
|
||||
}
|
||||
|
||||
f.Arguments = indent(f.Arguments)
|
||||
f.Options = indent(f.Options)
|
||||
f.Synopsis = indent(f.Synopsis)
|
||||
f.Subcommands = indent(f.Subcommands)
|
||||
f.Description = indent(f.Description)
|
||||
}
|
||||
|
||||
const usageFormat = "{{if .Usage}}{{.Usage}}{{else}}{{.Path}}{{if .ArgUsage}} {{.ArgUsage}}{{end}} - {{.Tagline}}{{end}}"
|
||||
|
||||
const longHelpFormat = `USAGE
|
||||
{{.Indent}}{{template "usage" .}}
|
||||
|
||||
{{if .Synopsis}}SYNOPSIS
|
||||
{{.Synopsis}}
|
||||
|
||||
{{end}}{{if .Arguments}}ARGUMENTS
|
||||
|
||||
{{.Arguments}}
|
||||
|
||||
{{end}}{{if .Options}}OPTIONS
|
||||
|
||||
{{.Options}}
|
||||
|
||||
{{end}}{{if .Description}}DESCRIPTION
|
||||
|
||||
{{.Description}}
|
||||
|
||||
{{end}}{{if .Subcommands}}SUBCOMMANDS
|
||||
{{.Subcommands}}
|
||||
|
||||
{{.Indent}}Use '{{.Path}} <subcmd> --help' for more information about each command.
|
||||
{{end}}
|
||||
`
|
||||
const shortHelpFormat = `USAGE
|
||||
{{.Indent}}{{template "usage" .}}
|
||||
{{if .Synopsis}}
|
||||
{{.Synopsis}}
|
||||
{{end}}{{if .Description}}
|
||||
{{.Description}}
|
||||
{{end}}{{if .Subcommands}}
|
||||
SUBCOMMANDS
|
||||
{{.Subcommands}}
|
||||
{{end}}{{if .MoreHelp}}
|
||||
Use '{{.Path}} --help' for more information about this command.
|
||||
{{end}}
|
||||
`
|
||||
|
||||
var usageTemplate *template.Template
|
||||
var longHelpTemplate *template.Template
|
||||
var shortHelpTemplate *template.Template
|
||||
|
||||
func init() {
|
||||
usageTemplate = template.Must(template.New("usage").Parse(usageFormat))
|
||||
longHelpTemplate = template.Must(usageTemplate.New("longHelp").Parse(longHelpFormat))
|
||||
shortHelpTemplate = template.Must(usageTemplate.New("shortHelp").Parse(shortHelpFormat))
|
||||
}
|
||||
|
||||
// LongHelp writes a formatted CLI helptext string to a Writer for the given command
|
||||
func LongHelp(rootName string, root *cmds.Command, path []string, out io.Writer) error {
|
||||
cmd, err := root.Get(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pathStr := rootName
|
||||
if len(path) > 0 {
|
||||
pathStr += " " + strings.Join(path, " ")
|
||||
}
|
||||
|
||||
fields := helpFields{
|
||||
Indent: indentStr,
|
||||
Path: pathStr,
|
||||
ArgUsage: usageText(cmd),
|
||||
Tagline: cmd.Helptext.Tagline,
|
||||
Arguments: cmd.Helptext.Arguments,
|
||||
Options: cmd.Helptext.Options,
|
||||
Synopsis: cmd.Helptext.Synopsis,
|
||||
Subcommands: cmd.Helptext.Subcommands,
|
||||
Description: cmd.Helptext.ShortDescription,
|
||||
Usage: cmd.Helptext.Usage,
|
||||
MoreHelp: (cmd != root),
|
||||
}
|
||||
|
||||
if len(cmd.Helptext.LongDescription) > 0 {
|
||||
fields.Description = cmd.Helptext.LongDescription
|
||||
}
|
||||
|
||||
// autogen fields that are empty
|
||||
if len(fields.Arguments) == 0 {
|
||||
fields.Arguments = strings.Join(argumentText(cmd), "\n")
|
||||
}
|
||||
if len(fields.Options) == 0 {
|
||||
fields.Options = strings.Join(optionText(cmd), "\n")
|
||||
}
|
||||
if len(fields.Subcommands) == 0 {
|
||||
fields.Subcommands = strings.Join(subcommandText(cmd, rootName, path), "\n")
|
||||
}
|
||||
if len(fields.Synopsis) == 0 {
|
||||
fields.Synopsis = generateSynopsis(cmd, pathStr)
|
||||
}
|
||||
|
||||
// trim the extra newlines (see TrimNewlines doc)
|
||||
fields.TrimNewlines()
|
||||
|
||||
// indent all fields that have been set
|
||||
fields.IndentAll()
|
||||
|
||||
return longHelpTemplate.Execute(out, fields)
|
||||
}
|
||||
|
||||
// ShortHelp writes a formatted CLI helptext string to a Writer for the given command
|
||||
func ShortHelp(rootName string, root *cmds.Command, path []string, out io.Writer) error {
|
||||
cmd, err := root.Get(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// default cmd to root if there is no path
|
||||
if path == nil && cmd == nil {
|
||||
cmd = root
|
||||
}
|
||||
|
||||
pathStr := rootName
|
||||
if len(path) > 0 {
|
||||
pathStr += " " + strings.Join(path, " ")
|
||||
}
|
||||
|
||||
fields := helpFields{
|
||||
Indent: indentStr,
|
||||
Path: pathStr,
|
||||
ArgUsage: usageText(cmd),
|
||||
Tagline: cmd.Helptext.Tagline,
|
||||
Synopsis: cmd.Helptext.Synopsis,
|
||||
Description: cmd.Helptext.ShortDescription,
|
||||
Subcommands: cmd.Helptext.Subcommands,
|
||||
Usage: cmd.Helptext.Usage,
|
||||
MoreHelp: (cmd != root),
|
||||
}
|
||||
|
||||
// autogen fields that are empty
|
||||
if len(fields.Subcommands) == 0 {
|
||||
fields.Subcommands = strings.Join(subcommandText(cmd, rootName, path), "\n")
|
||||
}
|
||||
if len(fields.Synopsis) == 0 {
|
||||
fields.Synopsis = generateSynopsis(cmd, pathStr)
|
||||
}
|
||||
|
||||
// trim the extra newlines (see TrimNewlines doc)
|
||||
fields.TrimNewlines()
|
||||
|
||||
// indent all fields that have been set
|
||||
fields.IndentAll()
|
||||
|
||||
return shortHelpTemplate.Execute(out, fields)
|
||||
}
|
||||
|
||||
func generateSynopsis(cmd *cmds.Command, path string) string {
|
||||
res := path
|
||||
for _, opt := range cmd.Options {
|
||||
valopt, ok := cmd.Helptext.SynopsisOptionsValues[opt.Names()[0]]
|
||||
if !ok {
|
||||
valopt = opt.Names()[0]
|
||||
}
|
||||
sopt := ""
|
||||
for i, n := range opt.Names() {
|
||||
pre := "-"
|
||||
if len(n) > 1 {
|
||||
pre = "--"
|
||||
}
|
||||
if opt.Type() == cmdkit.Bool && opt.Default() == true {
|
||||
pre = "--"
|
||||
sopt = fmt.Sprintf("%s%s=false", pre, n)
|
||||
break
|
||||
} else {
|
||||
if i == 0 {
|
||||
if opt.Type() == cmdkit.Bool {
|
||||
sopt = fmt.Sprintf("%s%s", pre, n)
|
||||
} else {
|
||||
sopt = fmt.Sprintf("%s%s=<%s>", pre, n, valopt)
|
||||
}
|
||||
} else {
|
||||
sopt = fmt.Sprintf("%s | %s%s", sopt, pre, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
res = fmt.Sprintf("%s [%s]", res, sopt)
|
||||
}
|
||||
if len(cmd.Arguments) > 0 {
|
||||
res = fmt.Sprintf("%s [--]", res)
|
||||
}
|
||||
for _, arg := range cmd.Arguments {
|
||||
sarg := fmt.Sprintf("<%s>", arg.Name)
|
||||
if arg.Variadic {
|
||||
sarg = sarg + "..."
|
||||
}
|
||||
|
||||
if !arg.Required {
|
||||
sarg = fmt.Sprintf("[%s]", sarg)
|
||||
}
|
||||
res = fmt.Sprintf("%s %s", res, sarg)
|
||||
}
|
||||
return strings.Trim(res, " ")
|
||||
}
|
||||
|
||||
func argumentText(cmd *cmds.Command) []string {
|
||||
lines := make([]string, len(cmd.Arguments))
|
||||
|
||||
for i, arg := range cmd.Arguments {
|
||||
lines[i] = argUsageText(arg)
|
||||
}
|
||||
lines = align(lines)
|
||||
for i, arg := range cmd.Arguments {
|
||||
lines[i] += " - " + arg.Description
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
func optionFlag(flag string) string {
|
||||
if len(flag) == 1 {
|
||||
return fmt.Sprintf(shortFlag, flag)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(longFlag, flag)
|
||||
}
|
||||
|
||||
func optionText(cmd ...*cmds.Command) []string {
|
||||
// get a slice of the options we want to list out
|
||||
options := make([]cmdkit.Option, 0)
|
||||
for _, c := range cmd {
|
||||
options = append(options, c.Options...)
|
||||
}
|
||||
|
||||
// add option names to output (with each name aligned)
|
||||
lines := make([]string, 0)
|
||||
j := 0
|
||||
for {
|
||||
done := true
|
||||
i := 0
|
||||
for _, opt := range options {
|
||||
if len(lines) < i+1 {
|
||||
lines = append(lines, "")
|
||||
}
|
||||
|
||||
names := sortByLength(opt.Names())
|
||||
if len(names) >= j+1 {
|
||||
lines[i] += optionFlag(names[j])
|
||||
}
|
||||
if len(names) > j+1 {
|
||||
lines[i] += ", "
|
||||
done = false
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
if done {
|
||||
break
|
||||
}
|
||||
|
||||
lines = align(lines)
|
||||
j++
|
||||
}
|
||||
lines = align(lines)
|
||||
|
||||
// add option types to output
|
||||
for i, opt := range options {
|
||||
lines[i] += " " + fmt.Sprintf("%v", opt.Type())
|
||||
}
|
||||
lines = align(lines)
|
||||
|
||||
// add option descriptions to output
|
||||
for i, opt := range options {
|
||||
lines[i] += " - " + opt.Description()
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
func subcommandText(cmd *cmds.Command, rootName string, path []string) []string {
|
||||
prefix := fmt.Sprintf("%v %v", rootName, strings.Join(path, " "))
|
||||
if len(path) > 0 {
|
||||
prefix += " "
|
||||
}
|
||||
|
||||
// Sorting fixes changing order bug #2981.
|
||||
sortedNames := make([]string, 0)
|
||||
for name := range cmd.Subcommands {
|
||||
sortedNames = append(sortedNames, name)
|
||||
}
|
||||
sort.Strings(sortedNames)
|
||||
|
||||
subcmds := make([]*cmds.Command, len(cmd.Subcommands))
|
||||
lines := make([]string, len(cmd.Subcommands))
|
||||
|
||||
for i, name := range sortedNames {
|
||||
sub := cmd.Subcommands[name]
|
||||
usage := usageText(sub)
|
||||
if len(usage) > 0 {
|
||||
usage = " " + usage
|
||||
}
|
||||
lines[i] = prefix + name + usage
|
||||
subcmds[i] = sub
|
||||
}
|
||||
|
||||
lines = align(lines)
|
||||
for i, sub := range subcmds {
|
||||
lines[i] += " - " + sub.Helptext.Tagline
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
func usageText(cmd *cmds.Command) string {
|
||||
s := ""
|
||||
for i, arg := range cmd.Arguments {
|
||||
if i != 0 {
|
||||
s += " "
|
||||
}
|
||||
s += argUsageText(arg)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func argUsageText(arg cmdkit.Argument) string {
|
||||
s := arg.Name
|
||||
|
||||
if arg.Required {
|
||||
s = fmt.Sprintf(requiredArg, s)
|
||||
} else {
|
||||
s = fmt.Sprintf(optionalArg, s)
|
||||
}
|
||||
|
||||
if arg.Variadic {
|
||||
s = fmt.Sprintf(variadicArg, s)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func align(lines []string) []string {
|
||||
longest := 0
|
||||
for _, line := range lines {
|
||||
length := len(line)
|
||||
if length > longest {
|
||||
longest = length
|
||||
}
|
||||
}
|
||||
|
||||
for i, line := range lines {
|
||||
length := len(line)
|
||||
if length > 0 {
|
||||
lines[i] += strings.Repeat(" ", longest-length)
|
||||
}
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
func indentString(line string, prefix string) string {
|
||||
return prefix + strings.Replace(line, "\n", "\n"+prefix, -1)
|
||||
}
|
||||
|
||||
type lengthSlice []string
|
||||
|
||||
func (ls lengthSlice) Len() int {
|
||||
return len(ls)
|
||||
}
|
||||
func (ls lengthSlice) Swap(a, b int) {
|
||||
ls[a], ls[b] = ls[b], ls[a]
|
||||
}
|
||||
func (ls lengthSlice) Less(a, b int) bool {
|
||||
return len(ls[a]) < len(ls[b])
|
||||
}
|
||||
|
||||
func sortByLength(slice []string) []string {
|
||||
output := make(lengthSlice, len(slice))
|
||||
for i, val := range slice {
|
||||
output[i] = val
|
||||
}
|
||||
sort.Sort(output)
|
||||
return []string(output)
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
cmds "github.com/ipfs/go-ipfs/commands"
|
||||
|
||||
"gx/ipfs/QmUyfy4QSr3NXym4etEiRyxBLqqAeKHJuRdi8AACxg63fZ/go-ipfs-cmdkit"
|
||||
)
|
||||
|
||||
func TestSynopsisGenerator(t *testing.T) {
|
||||
command := &cmds.Command{
|
||||
Arguments: []cmdkit.Argument{
|
||||
cmdkit.StringArg("required", true, false, ""),
|
||||
cmdkit.StringArg("variadic", false, true, ""),
|
||||
},
|
||||
Options: []cmdkit.Option{
|
||||
cmdkit.StringOption("opt", "o", "Option"),
|
||||
},
|
||||
Helptext: cmdkit.HelpText{
|
||||
SynopsisOptionsValues: map[string]string{
|
||||
"opt": "OPTION",
|
||||
},
|
||||
},
|
||||
}
|
||||
syn := generateSynopsis(command, "cmd")
|
||||
t.Logf("Synopsis is: %s", syn)
|
||||
if !strings.HasPrefix(syn, "cmd ") {
|
||||
t.Fatal("Synopsis should start with command name")
|
||||
}
|
||||
if !strings.Contains(syn, "[--opt=<OPTION> | -o]") {
|
||||
t.Fatal("Synopsis should contain option descriptor")
|
||||
}
|
||||
if !strings.Contains(syn, "<required>") {
|
||||
t.Fatal("Synopsis should contain required argument")
|
||||
}
|
||||
if !strings.Contains(syn, "<variadic>...") {
|
||||
t.Fatal("Synopsis should contain variadic argument")
|
||||
}
|
||||
if !strings.Contains(syn, "[<variadic>...]") {
|
||||
t.Fatal("Synopsis should contain optional argument")
|
||||
}
|
||||
if !strings.Contains(syn, "[--]") {
|
||||
t.Fatal("Synopsis should contain options finalizer")
|
||||
}
|
||||
}
|
@ -1,526 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
cmds "github.com/ipfs/go-ipfs/commands"
|
||||
u "gx/ipfs/QmSU6eubNdhXjFBJBSksTp8kv8YRub8mGAPv8tVJHmL2EU/go-ipfs-util"
|
||||
logging "gx/ipfs/QmSpJByNKFX1sCsHBEp3R73FL4NF6FnQTEGyNAXHm2GS52/go-log"
|
||||
"gx/ipfs/QmUyfy4QSr3NXym4etEiRyxBLqqAeKHJuRdi8AACxg63fZ/go-ipfs-cmdkit"
|
||||
"gx/ipfs/QmUyfy4QSr3NXym4etEiRyxBLqqAeKHJuRdi8AACxg63fZ/go-ipfs-cmdkit/files"
|
||||
osh "gx/ipfs/QmXuBJ7DR6k3rmUEKtvVMhwjmXDuJgXXPUt4LQXKBMsU93/go-os-helper"
|
||||
)
|
||||
|
||||
var log = logging.Logger("commands/cli")
|
||||
|
||||
// Parse parses the input commandline string (cmd, flags, and args).
|
||||
// returns the corresponding command Request object.
|
||||
func Parse(input []string, stdin *os.File, root *cmds.Command) (cmds.Request, *cmds.Command, []string, error) {
|
||||
path, opts, stringVals, cmd, err := parseOpts(input, root)
|
||||
if err != nil {
|
||||
return nil, nil, path, err
|
||||
}
|
||||
|
||||
optDefs, err := root.GetOptions(path)
|
||||
if err != nil {
|
||||
return nil, cmd, path, err
|
||||
}
|
||||
|
||||
req, err := cmds.NewRequest(path, opts, nil, nil, cmd, optDefs)
|
||||
if err != nil {
|
||||
return nil, cmd, path, err
|
||||
}
|
||||
|
||||
// This is an ugly hack to maintain our current CLI interface while fixing
|
||||
// other stdin usage bugs. Let this serve as a warning, be careful about the
|
||||
// choices you make, they will haunt you forever.
|
||||
if len(path) == 2 && path[0] == "bootstrap" {
|
||||
if (path[1] == "add" && opts["default"] == true) ||
|
||||
(path[1] == "rm" && opts["all"] == true) {
|
||||
stdin = nil
|
||||
}
|
||||
}
|
||||
|
||||
stringArgs, fileArgs, err := ParseArgs(req, stringVals, stdin, cmd.Arguments, root)
|
||||
if err != nil {
|
||||
return req, cmd, path, err
|
||||
}
|
||||
req.SetArguments(stringArgs)
|
||||
|
||||
if len(fileArgs) > 0 {
|
||||
file := files.NewSliceFile("", "", fileArgs)
|
||||
req.SetFiles(file)
|
||||
}
|
||||
|
||||
err = cmd.CheckArguments(req)
|
||||
|
||||
return req, cmd, path, err
|
||||
}
|
||||
|
||||
func ParseArgs(req cmds.Request, inputs []string, stdin *os.File, argDefs []cmdkit.Argument, root *cmds.Command) ([]string, []files.File, error) {
|
||||
var err error
|
||||
|
||||
// if -r is provided, and it is associated with the package builtin
|
||||
// recursive path option, allow recursive file paths
|
||||
recursiveOpt := req.Option(cmdkit.RecShort)
|
||||
recursive := false
|
||||
if recursiveOpt != nil && recursiveOpt.Definition() == cmdkit.OptionRecursivePath {
|
||||
recursive, _, err = recursiveOpt.Bool()
|
||||
if err != nil {
|
||||
return nil, nil, u.ErrCast()
|
||||
}
|
||||
}
|
||||
|
||||
// if '--hidden' is provided, enumerate hidden paths
|
||||
hiddenOpt := req.Option("hidden")
|
||||
hidden := false
|
||||
if hiddenOpt != nil {
|
||||
hidden, _, err = hiddenOpt.Bool()
|
||||
if err != nil {
|
||||
return nil, nil, u.ErrCast()
|
||||
}
|
||||
}
|
||||
return parseArgs(inputs, stdin, argDefs, recursive, hidden, root)
|
||||
}
|
||||
|
||||
// Parse a command line made up of sub-commands, short arguments, long arguments and positional arguments
|
||||
func parseOpts(args []string, root *cmds.Command) (
|
||||
path []string,
|
||||
opts map[string]interface{},
|
||||
stringVals []string,
|
||||
cmd *cmds.Command,
|
||||
err error,
|
||||
) {
|
||||
path = make([]string, 0, len(args))
|
||||
stringVals = make([]string, 0, len(args))
|
||||
optDefs := map[string]cmdkit.Option{}
|
||||
opts = map[string]interface{}{}
|
||||
cmd = root
|
||||
|
||||
// parseFlag checks that a flag is valid and saves it into opts
|
||||
// Returns true if the optional second argument is used
|
||||
parseFlag := func(name string, arg *string, mustUse bool) (bool, error) {
|
||||
if _, ok := opts[name]; ok {
|
||||
return false, fmt.Errorf("Duplicate values for option '%s'", name)
|
||||
}
|
||||
|
||||
optDef, found := optDefs[name]
|
||||
if !found {
|
||||
err = fmt.Errorf("Unrecognized option '%s'", name)
|
||||
return false, err
|
||||
}
|
||||
// mustUse implies that you must use the argument given after the '='
|
||||
// eg. -r=true means you must take true into consideration
|
||||
// mustUse == true in the above case
|
||||
// eg. ipfs -r <file> means disregard <file> since there is no '='
|
||||
// mustUse == false in the above situation
|
||||
//arg == nil implies the flag was specified without an argument
|
||||
if optDef.Type() == cmdkit.Bool {
|
||||
if arg == nil || !mustUse {
|
||||
opts[name] = true
|
||||
return false, nil
|
||||
}
|
||||
argVal := strings.ToLower(*arg)
|
||||
switch argVal {
|
||||
case "true":
|
||||
opts[name] = true
|
||||
return true, nil
|
||||
case "false":
|
||||
opts[name] = false
|
||||
return true, nil
|
||||
default:
|
||||
return true, fmt.Errorf("Option '%s' takes true/false arguments, but was passed '%s'", name, argVal)
|
||||
}
|
||||
} else {
|
||||
if arg == nil {
|
||||
return true, fmt.Errorf("Missing argument for option '%s'", name)
|
||||
}
|
||||
opts[name] = *arg
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
optDefs, err = root.GetOptions(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
consumed := false
|
||||
for i, arg := range args {
|
||||
switch {
|
||||
case consumed:
|
||||
// arg was already consumed by the preceding flag
|
||||
consumed = false
|
||||
continue
|
||||
|
||||
case arg == "--":
|
||||
// treat all remaining arguments as positional arguments
|
||||
stringVals = append(stringVals, args[i+1:]...)
|
||||
return
|
||||
|
||||
case strings.HasPrefix(arg, "--"):
|
||||
// arg is a long flag, with an optional argument specified
|
||||
// using `=' or in args[i+1]
|
||||
var slurped bool
|
||||
var next *string
|
||||
split := strings.SplitN(arg, "=", 2)
|
||||
if len(split) == 2 {
|
||||
slurped = false
|
||||
arg = split[0]
|
||||
next = &split[1]
|
||||
} else {
|
||||
slurped = true
|
||||
if i+1 < len(args) {
|
||||
next = &args[i+1]
|
||||
} else {
|
||||
next = nil
|
||||
}
|
||||
}
|
||||
consumed, err = parseFlag(arg[2:], next, len(split) == 2)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !slurped {
|
||||
consumed = false
|
||||
}
|
||||
|
||||
case strings.HasPrefix(arg, "-") && arg != "-":
|
||||
// args is one or more flags in short form, followed by an optional argument
|
||||
// all flags except the last one have type bool
|
||||
for arg = arg[1:]; len(arg) != 0; arg = arg[1:] {
|
||||
var rest *string
|
||||
var slurped bool
|
||||
mustUse := false
|
||||
if len(arg) > 1 {
|
||||
slurped = false
|
||||
str := arg[1:]
|
||||
if len(str) > 0 && str[0] == '=' {
|
||||
str = str[1:]
|
||||
mustUse = true
|
||||
}
|
||||
rest = &str
|
||||
} else {
|
||||
slurped = true
|
||||
if i+1 < len(args) {
|
||||
rest = &args[i+1]
|
||||
} else {
|
||||
rest = nil
|
||||
}
|
||||
}
|
||||
var end bool
|
||||
end, err = parseFlag(arg[:1], rest, mustUse)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if end {
|
||||
consumed = slurped
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
// arg is a sub-command or a positional argument
|
||||
sub := cmd.Subcommand(arg)
|
||||
if sub != nil {
|
||||
cmd = sub
|
||||
path = append(path, arg)
|
||||
optDefs, err = root.GetOptions(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// If we've come across an external binary call, pass all the remaining
|
||||
// arguments on to it
|
||||
if cmd.External {
|
||||
stringVals = append(stringVals, args[i+1:]...)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
stringVals = append(stringVals, arg)
|
||||
if len(path) == 0 {
|
||||
// found a typo or early argument
|
||||
err = printSuggestions(stringVals, root)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const msgStdinInfo = "ipfs: Reading from %s; send Ctrl-d to stop."
|
||||
|
||||
func parseArgs(inputs []string, stdin *os.File, argDefs []cmdkit.Argument, recursive, hidden bool, root *cmds.Command) ([]string, []files.File, error) {
|
||||
// ignore stdin on Windows
|
||||
if osh.IsWindows() {
|
||||
stdin = nil
|
||||
}
|
||||
|
||||
// count required argument definitions
|
||||
numRequired := 0
|
||||
for _, argDef := range argDefs {
|
||||
if argDef.Required {
|
||||
numRequired++
|
||||
}
|
||||
}
|
||||
|
||||
// count number of values provided by user.
|
||||
// if there is at least one ArgDef, we can safely trigger the inputs loop
|
||||
// below to parse stdin.
|
||||
numInputs := len(inputs)
|
||||
if len(argDefs) > 0 && argDefs[len(argDefs)-1].SupportsStdin && stdin != nil {
|
||||
numInputs++
|
||||
}
|
||||
|
||||
// if we have more arg values provided than argument definitions,
|
||||
// and the last arg definition is not variadic (or there are no definitions), return an error
|
||||
notVariadic := len(argDefs) == 0 || !argDefs[len(argDefs)-1].Variadic
|
||||
if notVariadic && len(inputs) > len(argDefs) {
|
||||
err := printSuggestions(inputs, root)
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
stringArgs := make([]string, 0, numInputs)
|
||||
|
||||
fileArgs := make(map[string]files.File)
|
||||
argDefIndex := 0 // the index of the current argument definition
|
||||
|
||||
for i := 0; i < numInputs; i++ {
|
||||
argDef := getArgDef(argDefIndex, argDefs)
|
||||
|
||||
// skip optional argument definitions if there aren't sufficient remaining inputs
|
||||
for numInputs-i <= numRequired && !argDef.Required {
|
||||
argDefIndex++
|
||||
argDef = getArgDef(argDefIndex, argDefs)
|
||||
}
|
||||
if argDef.Required {
|
||||
numRequired--
|
||||
}
|
||||
|
||||
fillingVariadic := argDefIndex+1 > len(argDefs)
|
||||
switch argDef.Type {
|
||||
case cmdkit.ArgString:
|
||||
if len(inputs) > 0 {
|
||||
stringArgs, inputs = append(stringArgs, inputs[0]), inputs[1:]
|
||||
} else if stdin != nil && argDef.SupportsStdin && !fillingVariadic {
|
||||
if r, err := maybeWrapStdin(stdin, msgStdinInfo); err == nil {
|
||||
fileArgs[stdin.Name()] = files.NewReaderFile("stdin", "", r, nil)
|
||||
stdin = nil
|
||||
}
|
||||
}
|
||||
case cmdkit.ArgFile:
|
||||
if len(inputs) > 0 {
|
||||
// treat stringArg values as file paths
|
||||
fpath := inputs[0]
|
||||
inputs = inputs[1:]
|
||||
var file files.File
|
||||
if fpath == "-" {
|
||||
r, err := maybeWrapStdin(stdin, msgStdinInfo)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
fpath = stdin.Name()
|
||||
file = files.NewReaderFile("", fpath, r, nil)
|
||||
} else {
|
||||
nf, err := appendFile(fpath, argDef, recursive, hidden)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
file = nf
|
||||
}
|
||||
|
||||
fileArgs[fpath] = file
|
||||
} else if stdin != nil && argDef.SupportsStdin &&
|
||||
argDef.Required && !fillingVariadic {
|
||||
r, err := maybeWrapStdin(stdin, msgStdinInfo)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
fpath := stdin.Name()
|
||||
fileArgs[fpath] = files.NewReaderFile("", fpath, r, nil)
|
||||
}
|
||||
}
|
||||
|
||||
argDefIndex++
|
||||
}
|
||||
|
||||
// check to make sure we didn't miss any required arguments
|
||||
if len(argDefs) > argDefIndex {
|
||||
for _, argDef := range argDefs[argDefIndex:] {
|
||||
if argDef.Required {
|
||||
return nil, nil, fmt.Errorf("Argument '%s' is required", argDef.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return stringArgs, filesMapToSortedArr(fileArgs), nil
|
||||
}
|
||||
|
||||
func filesMapToSortedArr(fs map[string]files.File) []files.File {
|
||||
var names []string
|
||||
for name := range fs {
|
||||
names = append(names, name)
|
||||
}
|
||||
|
||||
sort.Strings(names)
|
||||
|
||||
var out []files.File
|
||||
for _, f := range names {
|
||||
out = append(out, fs[f])
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func getArgDef(i int, argDefs []cmdkit.Argument) *cmdkit.Argument {
|
||||
if i < len(argDefs) {
|
||||
// get the argument definition (usually just argDefs[i])
|
||||
return &argDefs[i]
|
||||
|
||||
} else if len(argDefs) > 0 {
|
||||
// but if i > len(argDefs) we use the last argument definition)
|
||||
return &argDefs[len(argDefs)-1]
|
||||
}
|
||||
|
||||
// only happens if there aren't any definitions
|
||||
return nil
|
||||
}
|
||||
|
||||
const notRecursiveFmtStr = "'%s' is a directory, use the '-%s' flag to specify directories"
|
||||
const dirNotSupportedFmtStr = "Invalid path '%s', argument '%s' does not support directories"
|
||||
const winDriveLetterFmtStr = "%q is a drive letter, not a drive path"
|
||||
|
||||
func appendFile(fpath string, argDef *cmdkit.Argument, recursive, hidden bool) (files.File, error) {
|
||||
// resolve Windows relative dot paths like `X:.\somepath`
|
||||
if osh.IsWindows() {
|
||||
if len(fpath) >= 3 && fpath[1:3] == ":." {
|
||||
var err error
|
||||
fpath, err = filepath.Abs(fpath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if fpath == "." {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cwd, err = filepath.EvalSymlinks(cwd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fpath = cwd
|
||||
}
|
||||
|
||||
fpath = filepath.Clean(fpath)
|
||||
|
||||
stat, err := os.Lstat(fpath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if stat.IsDir() {
|
||||
if !argDef.Recursive {
|
||||
return nil, fmt.Errorf(dirNotSupportedFmtStr, fpath, argDef.Name)
|
||||
}
|
||||
if !recursive {
|
||||
return nil, fmt.Errorf(notRecursiveFmtStr, fpath, cmdkit.RecShort)
|
||||
}
|
||||
}
|
||||
|
||||
if osh.IsWindows() {
|
||||
return windowsParseFile(fpath, hidden, stat)
|
||||
}
|
||||
|
||||
return files.NewSerialFile(path.Base(fpath), fpath, hidden, stat)
|
||||
}
|
||||
|
||||
// Inform the user if a file is waiting on input
|
||||
func maybeWrapStdin(f *os.File, msg string) (io.ReadCloser, error) {
|
||||
isTty, err := isTty(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if isTty {
|
||||
return newMessageReader(f, fmt.Sprintf(msg, f.Name())), nil
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func isTty(f *os.File) (bool, error) {
|
||||
fInfo, err := f.Stat()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
return (fInfo.Mode() & os.ModeCharDevice) != 0, nil
|
||||
}
|
||||
|
||||
type messageReader struct {
|
||||
r io.ReadCloser
|
||||
done bool
|
||||
message string
|
||||
}
|
||||
|
||||
func newMessageReader(r io.ReadCloser, msg string) io.ReadCloser {
|
||||
return &messageReader{
|
||||
r: r,
|
||||
message: msg,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *messageReader) Read(b []byte) (int, error) {
|
||||
if !r.done {
|
||||
fmt.Fprintln(os.Stderr, r.message)
|
||||
r.done = true
|
||||
}
|
||||
|
||||
return r.r.Read(b)
|
||||
}
|
||||
|
||||
func (r *messageReader) Close() error {
|
||||
return r.r.Close()
|
||||
}
|
||||
|
||||
func windowsParseFile(fpath string, hidden bool, stat os.FileInfo) (files.File, error) {
|
||||
// special cases for Windows drive roots i.e. `X:\` and their long form `\\?\X:\`
|
||||
// drive path must be preserved as `X:\` (or it's longform) and not converted to `X:`, `X:.`, `\`, or `/` here
|
||||
switch len(fpath) {
|
||||
case 3:
|
||||
// `X:` is cleaned to `X:.` which may not be the expected behaviour by the user, they'll need to provide more specific input
|
||||
if fpath[1:3] == ":." {
|
||||
return nil, fmt.Errorf(winDriveLetterFmtStr, fpath[:2])
|
||||
}
|
||||
// `X:\` needs to preserve the `\`, path.Base(filepath.ToSlash(fpath)) results in `X:` which is not valid
|
||||
if fpath[1:3] == ":\\" {
|
||||
return files.NewSerialFile(fpath, fpath, hidden, stat)
|
||||
}
|
||||
case 6:
|
||||
// `\\?\X:` long prefix form of `X:`, still ambiguous
|
||||
if fpath[:4] == "\\\\?\\" && fpath[5] == ':' {
|
||||
return nil, fmt.Errorf(winDriveLetterFmtStr, fpath)
|
||||
}
|
||||
case 7:
|
||||
// `\\?\X:\` long prefix form is translated into short form `X:\`
|
||||
if fpath[:4] == "\\\\?\\" && fpath[5] == ':' && fpath[6] == '\\' {
|
||||
fpath = string(fpath[4]) + ":\\"
|
||||
return files.NewSerialFile(fpath, fpath, hidden, stat)
|
||||
}
|
||||
}
|
||||
|
||||
return files.NewSerialFile(path.Base(filepath.ToSlash(fpath)), fpath, hidden, stat)
|
||||
}
|
@ -1,313 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ipfs/go-ipfs/commands"
|
||||
|
||||
"gx/ipfs/QmUyfy4QSr3NXym4etEiRyxBLqqAeKHJuRdi8AACxg63fZ/go-ipfs-cmdkit"
|
||||
)
|
||||
|
||||
type kvs map[string]interface{}
|
||||
type words []string
|
||||
|
||||
func sameWords(a words, b words) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i, w := range a {
|
||||
if w != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func sameKVs(a kvs, b kvs) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for k, v := range a {
|
||||
if v != b[k] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestSameWords(t *testing.T) {
|
||||
a := []string{"v1", "v2"}
|
||||
b := []string{"v1", "v2", "v3"}
|
||||
c := []string{"v2", "v3"}
|
||||
d := []string{"v2"}
|
||||
e := []string{"v2", "v3"}
|
||||
f := []string{"v2", "v1"}
|
||||
|
||||
test := func(a words, b words, v bool) {
|
||||
if sameWords(a, b) != v {
|
||||
t.Errorf("sameWords('%v', '%v') != %v", a, b, v)
|
||||
}
|
||||
}
|
||||
|
||||
test(a, b, false)
|
||||
test(a, a, true)
|
||||
test(a, c, false)
|
||||
test(b, c, false)
|
||||
test(c, d, false)
|
||||
test(c, e, true)
|
||||
test(b, e, false)
|
||||
test(a, b, false)
|
||||
test(a, f, false)
|
||||
test(e, f, false)
|
||||
test(f, f, true)
|
||||
}
|
||||
|
||||
func TestOptionParsing(t *testing.T) {
|
||||
subCmd := &commands.Command{}
|
||||
cmd := &commands.Command{
|
||||
Options: []cmdkit.Option{
|
||||
cmdkit.StringOption("string", "s", "a string"),
|
||||
cmdkit.BoolOption("bool", "b", "a bool"),
|
||||
},
|
||||
Subcommands: map[string]*commands.Command{
|
||||
"test": subCmd,
|
||||
},
|
||||
}
|
||||
|
||||
testHelper := func(args string, expectedOpts kvs, expectedWords words, expectErr bool) {
|
||||
var opts map[string]interface{}
|
||||
var input []string
|
||||
|
||||
_, opts, input, _, err := parseOpts(strings.Split(args, " "), cmd)
|
||||
if expectErr {
|
||||
if err == nil {
|
||||
t.Errorf("Command line '%v' parsing should have failed", args)
|
||||
}
|
||||
} else if err != nil {
|
||||
t.Errorf("Command line '%v' failed to parse: %v", args, err)
|
||||
} else if !sameWords(input, expectedWords) || !sameKVs(opts, expectedOpts) {
|
||||
t.Errorf("Command line '%v':\n parsed as %v %v\n instead of %v %v",
|
||||
args, opts, input, expectedOpts, expectedWords)
|
||||
}
|
||||
}
|
||||
|
||||
testFail := func(args string) {
|
||||
testHelper(args, kvs{}, words{}, true)
|
||||
}
|
||||
|
||||
test := func(args string, expectedOpts kvs, expectedWords words) {
|
||||
testHelper(args, expectedOpts, expectedWords, false)
|
||||
}
|
||||
|
||||
test("test -", kvs{}, words{"-"})
|
||||
testFail("-b -b")
|
||||
test("test beep boop", kvs{}, words{"beep", "boop"})
|
||||
testFail("-s")
|
||||
test("-s foo", kvs{"s": "foo"}, words{})
|
||||
test("-sfoo", kvs{"s": "foo"}, words{})
|
||||
test("-s=foo", kvs{"s": "foo"}, words{})
|
||||
test("-b", kvs{"b": true}, words{})
|
||||
test("-bs foo", kvs{"b": true, "s": "foo"}, words{})
|
||||
test("-sb", kvs{"s": "b"}, words{})
|
||||
test("-b test foo", kvs{"b": true}, words{"foo"})
|
||||
test("--bool test foo", kvs{"bool": true}, words{"foo"})
|
||||
testFail("--bool=foo")
|
||||
testFail("--string")
|
||||
test("--string foo", kvs{"string": "foo"}, words{})
|
||||
test("--string=foo", kvs{"string": "foo"}, words{})
|
||||
test("-- -b", kvs{}, words{"-b"})
|
||||
test("test foo -b", kvs{"b": true}, words{"foo"})
|
||||
test("-b=false", kvs{"b": false}, words{})
|
||||
test("-b=true", kvs{"b": true}, words{})
|
||||
test("-b=false test foo", kvs{"b": false}, words{"foo"})
|
||||
test("-b=true test foo", kvs{"b": true}, words{"foo"})
|
||||
test("--bool=true test foo", kvs{"bool": true}, words{"foo"})
|
||||
test("--bool=false test foo", kvs{"bool": false}, words{"foo"})
|
||||
test("-b test true", kvs{"b": true}, words{"true"})
|
||||
test("-b test false", kvs{"b": true}, words{"false"})
|
||||
test("-b=FaLsE test foo", kvs{"b": false}, words{"foo"})
|
||||
test("-b=TrUe test foo", kvs{"b": true}, words{"foo"})
|
||||
test("-b test true", kvs{"b": true}, words{"true"})
|
||||
test("-b test false", kvs{"b": true}, words{"false"})
|
||||
test("-b --string foo test bar", kvs{"b": true, "string": "foo"}, words{"bar"})
|
||||
test("-b=false --string bar", kvs{"b": false, "string": "bar"}, words{})
|
||||
testFail("foo test")
|
||||
}
|
||||
|
||||
func TestArgumentParsing(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("stdin handling doesnt yet work on windows")
|
||||
}
|
||||
rootCmd := &commands.Command{
|
||||
Subcommands: map[string]*commands.Command{
|
||||
"noarg": {},
|
||||
"onearg": {
|
||||
Arguments: []cmdkit.Argument{
|
||||
cmdkit.StringArg("a", true, false, "some arg"),
|
||||
},
|
||||
},
|
||||
"twoargs": {
|
||||
Arguments: []cmdkit.Argument{
|
||||
cmdkit.StringArg("a", true, false, "some arg"),
|
||||
cmdkit.StringArg("b", true, false, "another arg"),
|
||||
},
|
||||
},
|
||||
"variadic": {
|
||||
Arguments: []cmdkit.Argument{
|
||||
cmdkit.StringArg("a", true, true, "some arg"),
|
||||
},
|
||||
},
|
||||
"optional": {
|
||||
Arguments: []cmdkit.Argument{
|
||||
cmdkit.StringArg("b", false, true, "another arg"),
|
||||
},
|
||||
},
|
||||
"optionalsecond": {
|
||||
Arguments: []cmdkit.Argument{
|
||||
cmdkit.StringArg("a", true, false, "some arg"),
|
||||
cmdkit.StringArg("b", false, false, "another arg"),
|
||||
},
|
||||
},
|
||||
"reversedoptional": {
|
||||
Arguments: []cmdkit.Argument{
|
||||
cmdkit.StringArg("a", false, false, "some arg"),
|
||||
cmdkit.StringArg("b", true, false, "another arg"),
|
||||
},
|
||||
},
|
||||
"stdinenabled": {
|
||||
Arguments: []cmdkit.Argument{
|
||||
cmdkit.StringArg("a", true, true, "some arg").EnableStdin(),
|
||||
},
|
||||
},
|
||||
"stdinenabled2args": &commands.Command{
|
||||
Arguments: []cmdkit.Argument{
|
||||
cmdkit.StringArg("a", true, false, "some arg"),
|
||||
cmdkit.StringArg("b", true, true, "another arg").EnableStdin(),
|
||||
},
|
||||
},
|
||||
"stdinenablednotvariadic": &commands.Command{
|
||||
Arguments: []cmdkit.Argument{
|
||||
cmdkit.StringArg("a", true, false, "some arg").EnableStdin(),
|
||||
},
|
||||
},
|
||||
"stdinenablednotvariadic2args": &commands.Command{
|
||||
Arguments: []cmdkit.Argument{
|
||||
cmdkit.StringArg("a", true, false, "some arg"),
|
||||
cmdkit.StringArg("b", true, false, "another arg").EnableStdin(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
test := func(cmd words, f *os.File, res words) {
|
||||
if f != nil {
|
||||
if _, err := f.Seek(0, io.SeekStart); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
req, _, _, err := Parse(cmd, f, rootCmd)
|
||||
if err != nil {
|
||||
t.Errorf("Command '%v' should have passed parsing: %v", cmd, err)
|
||||
}
|
||||
if !sameWords(req.Arguments(), res) {
|
||||
t.Errorf("Arguments parsed from '%v' are '%v' instead of '%v'", cmd, req.Arguments(), res)
|
||||
}
|
||||
}
|
||||
|
||||
testFail := func(cmd words, fi *os.File, msg string) {
|
||||
_, _, _, err := Parse(cmd, nil, rootCmd)
|
||||
if err == nil {
|
||||
t.Errorf("Should have failed: %v", msg)
|
||||
}
|
||||
}
|
||||
|
||||
test([]string{"noarg"}, nil, []string{})
|
||||
testFail([]string{"noarg", "value!"}, nil, "provided an arg, but command didn't define any")
|
||||
|
||||
test([]string{"onearg", "value!"}, nil, []string{"value!"})
|
||||
testFail([]string{"onearg"}, nil, "didn't provide any args, arg is required")
|
||||
|
||||
test([]string{"twoargs", "value1", "value2"}, nil, []string{"value1", "value2"})
|
||||
testFail([]string{"twoargs", "value!"}, nil, "only provided 1 arg, needs 2")
|
||||
testFail([]string{"twoargs"}, nil, "didn't provide any args, 2 required")
|
||||
|
||||
test([]string{"variadic", "value!"}, nil, []string{"value!"})
|
||||
test([]string{"variadic", "value1", "value2", "value3"}, nil, []string{"value1", "value2", "value3"})
|
||||
testFail([]string{"variadic"}, nil, "didn't provide any args, 1 required")
|
||||
|
||||
test([]string{"optional", "value!"}, nil, []string{"value!"})
|
||||
test([]string{"optional"}, nil, []string{})
|
||||
test([]string{"optional", "value1", "value2"}, nil, []string{"value1", "value2"})
|
||||
|
||||
test([]string{"optionalsecond", "value!"}, nil, []string{"value!"})
|
||||
test([]string{"optionalsecond", "value1", "value2"}, nil, []string{"value1", "value2"})
|
||||
testFail([]string{"optionalsecond"}, nil, "didn't provide any args, 1 required")
|
||||
testFail([]string{"optionalsecond", "value1", "value2", "value3"}, nil, "provided too many args, takes 2 maximum")
|
||||
|
||||
test([]string{"reversedoptional", "value1", "value2"}, nil, []string{"value1", "value2"})
|
||||
test([]string{"reversedoptional", "value!"}, nil, []string{"value!"})
|
||||
|
||||
testFail([]string{"reversedoptional"}, nil, "didn't provide any args, 1 required")
|
||||
testFail([]string{"reversedoptional", "value1", "value2", "value3"}, nil, "provided too many args, only takes 1")
|
||||
|
||||
// Use a temp file to simulate stdin
|
||||
fileToSimulateStdin := func(t *testing.T, content string) *os.File {
|
||||
fstdin, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(fstdin.Name())
|
||||
|
||||
if _, err := io.WriteString(fstdin, content); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return fstdin
|
||||
}
|
||||
|
||||
test([]string{"stdinenabled", "value1", "value2"}, nil, []string{"value1", "value2"})
|
||||
|
||||
fstdin := fileToSimulateStdin(t, "stdin1")
|
||||
test([]string{"stdinenabled"}, fstdin, []string{"stdin1"})
|
||||
test([]string{"stdinenabled", "value1"}, fstdin, []string{"value1"})
|
||||
test([]string{"stdinenabled", "value1", "value2"}, fstdin, []string{"value1", "value2"})
|
||||
|
||||
fstdin = fileToSimulateStdin(t, "stdin1\nstdin2")
|
||||
test([]string{"stdinenabled"}, fstdin, []string{"stdin1", "stdin2"})
|
||||
|
||||
fstdin = fileToSimulateStdin(t, "stdin1\nstdin2\nstdin3")
|
||||
test([]string{"stdinenabled"}, fstdin, []string{"stdin1", "stdin2", "stdin3"})
|
||||
|
||||
test([]string{"stdinenabled2args", "value1", "value2"}, nil, []string{"value1", "value2"})
|
||||
|
||||
fstdin = fileToSimulateStdin(t, "stdin1")
|
||||
test([]string{"stdinenabled2args", "value1"}, fstdin, []string{"value1", "stdin1"})
|
||||
test([]string{"stdinenabled2args", "value1", "value2"}, fstdin, []string{"value1", "value2"})
|
||||
test([]string{"stdinenabled2args", "value1", "value2", "value3"}, fstdin, []string{"value1", "value2", "value3"})
|
||||
|
||||
fstdin = fileToSimulateStdin(t, "stdin1\nstdin2")
|
||||
test([]string{"stdinenabled2args", "value1"}, fstdin, []string{"value1", "stdin1", "stdin2"})
|
||||
|
||||
test([]string{"stdinenablednotvariadic", "value1"}, nil, []string{"value1"})
|
||||
|
||||
fstdin = fileToSimulateStdin(t, "stdin1")
|
||||
test([]string{"stdinenablednotvariadic"}, fstdin, []string{"stdin1"})
|
||||
test([]string{"stdinenablednotvariadic", "value1"}, fstdin, []string{"value1"})
|
||||
|
||||
test([]string{"stdinenablednotvariadic2args", "value1", "value2"}, nil, []string{"value1", "value2"})
|
||||
|
||||
fstdin = fileToSimulateStdin(t, "stdin1")
|
||||
test([]string{"stdinenablednotvariadic2args", "value1"}, fstdin, []string{"value1", "stdin1"})
|
||||
test([]string{"stdinenablednotvariadic2args", "value1", "value2"}, fstdin, []string{"value1", "value2"})
|
||||
testFail([]string{"stdinenablednotvariadic2args"}, fstdin, "cant use stdin for non stdin arg")
|
||||
|
||||
fstdin = fileToSimulateStdin(t, "stdin1")
|
||||
test([]string{"noarg"}, fstdin, []string{})
|
||||
|
||||
fstdin = fileToSimulateStdin(t, "stdin1")
|
||||
test([]string{"optionalsecond", "value1", "value2"}, fstdin, []string{"value1", "value2"})
|
||||
}
|
@ -1,310 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
cmds "github.com/ipfs/go-ipfs/commands"
|
||||
config "github.com/ipfs/go-ipfs/repo/config"
|
||||
|
||||
"gx/ipfs/QmUyfy4QSr3NXym4etEiRyxBLqqAeKHJuRdi8AACxg63fZ/go-ipfs-cmdkit"
|
||||
)
|
||||
|
||||
const (
|
||||
ApiUrlFormat = "http://%s%s/%s?%s"
|
||||
ApiPath = "/api/v0" // TODO: make configurable
|
||||
)
|
||||
|
||||
var OptionSkipMap = map[string]bool{
|
||||
"api": true,
|
||||
}
|
||||
|
||||
// Client is the commands HTTP client interface.
|
||||
type Client interface {
|
||||
Send(req cmds.Request) (cmds.Response, error)
|
||||
}
|
||||
|
||||
type client struct {
|
||||
serverAddress string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewClient(address string) Client {
|
||||
return &client{
|
||||
serverAddress: address,
|
||||
httpClient: http.DefaultClient,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *client) Send(req cmds.Request) (cmds.Response, error) {
|
||||
|
||||
if req.Context() == nil {
|
||||
log.Warningf("no context set in request")
|
||||
if err := req.SetRootContext(context.TODO()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// save user-provided encoding
|
||||
previousUserProvidedEncoding, found, err := req.Option(cmdkit.EncShort).String()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// override with json to send to server
|
||||
req.SetOption(cmdkit.EncShort, cmds.JSON)
|
||||
|
||||
// stream channel output
|
||||
req.SetOption(cmdkit.ChanOpt, "true")
|
||||
|
||||
query, err := getQuery(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var fileReader *MultiFileReader
|
||||
var reader io.Reader
|
||||
|
||||
if req.Files() != nil {
|
||||
fileReader = NewMultiFileReader(req.Files(), true)
|
||||
reader = fileReader
|
||||
}
|
||||
|
||||
path := strings.Join(req.Path(), "/")
|
||||
url := fmt.Sprintf(ApiUrlFormat, c.serverAddress, ApiPath, path, query)
|
||||
|
||||
httpReq, err := http.NewRequest("POST", url, reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO extract string consts?
|
||||
if fileReader != nil {
|
||||
httpReq.Header.Set(contentTypeHeader, "multipart/form-data; boundary="+fileReader.Boundary())
|
||||
} else {
|
||||
httpReq.Header.Set(contentTypeHeader, applicationOctetStream)
|
||||
}
|
||||
httpReq.Header.Set(uaHeader, config.ApiVersion)
|
||||
|
||||
httpReq.Cancel = req.Context().Done()
|
||||
httpReq.Close = true
|
||||
|
||||
httpRes, err := c.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// using the overridden JSON encoding in request
|
||||
res, err := getResponse(httpRes, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if found && len(previousUserProvidedEncoding) > 0 {
|
||||
// reset to user provided encoding after sending request
|
||||
// NB: if user has provided an encoding but it is the empty string,
|
||||
// still leave it as JSON.
|
||||
req.SetOption(cmdkit.EncShort, previousUserProvidedEncoding)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func getQuery(req cmds.Request) (string, error) {
|
||||
query := url.Values{}
|
||||
for k, v := range req.Options() {
|
||||
if OptionSkipMap[k] {
|
||||
continue
|
||||
}
|
||||
str := fmt.Sprintf("%v", v)
|
||||
query.Set(k, str)
|
||||
}
|
||||
|
||||
args := req.StringArguments()
|
||||
argDefs := req.Command().Arguments
|
||||
|
||||
argDefIndex := 0
|
||||
|
||||
for _, arg := range args {
|
||||
argDef := argDefs[argDefIndex]
|
||||
// skip ArgFiles
|
||||
for argDef.Type == cmdkit.ArgFile {
|
||||
argDefIndex++
|
||||
argDef = argDefs[argDefIndex]
|
||||
}
|
||||
|
||||
query.Add("arg", arg)
|
||||
|
||||
if len(argDefs) > argDefIndex+1 {
|
||||
argDefIndex++
|
||||
}
|
||||
}
|
||||
|
||||
return query.Encode(), nil
|
||||
}
|
||||
|
||||
// getResponse decodes a http.Response to create a cmds.Response
|
||||
func getResponse(httpRes *http.Response, req cmds.Request) (cmds.Response, error) {
|
||||
var err error
|
||||
res := cmds.NewResponse(req)
|
||||
|
||||
contentType := httpRes.Header.Get(contentTypeHeader)
|
||||
contentType = strings.Split(contentType, ";")[0]
|
||||
|
||||
lengthHeader := httpRes.Header.Get(extraContentLengthHeader)
|
||||
if len(lengthHeader) > 0 {
|
||||
length, err := strconv.ParseUint(lengthHeader, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.SetLength(length)
|
||||
}
|
||||
|
||||
rr := &httpResponseReader{httpRes}
|
||||
res.SetCloser(rr)
|
||||
|
||||
if contentType != applicationJson {
|
||||
// for all non json output types, just stream back the output
|
||||
res.SetOutput(rr)
|
||||
return res, nil
|
||||
|
||||
} else if len(httpRes.Header.Get(channelHeader)) > 0 {
|
||||
// if output is coming from a channel, decode each chunk
|
||||
outChan := make(chan interface{})
|
||||
|
||||
go readStreamedJson(req, rr, outChan, res)
|
||||
|
||||
res.SetOutput((<-chan interface{})(outChan))
|
||||
return res, nil
|
||||
}
|
||||
|
||||
dec := json.NewDecoder(rr)
|
||||
|
||||
// If we ran into an error
|
||||
if httpRes.StatusCode >= http.StatusBadRequest {
|
||||
var e *cmdkit.Error
|
||||
|
||||
switch {
|
||||
case httpRes.StatusCode == http.StatusNotFound:
|
||||
// handle 404s
|
||||
e = &cmdkit.Error{Message: "Command not found.", Code: cmdkit.ErrClient}
|
||||
|
||||
case contentType == plainText:
|
||||
// handle non-marshalled errors
|
||||
mes, err := ioutil.ReadAll(rr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e = &cmdkit.Error{Message: string(mes), Code: cmdkit.ErrNormal}
|
||||
default:
|
||||
// handle marshalled errors
|
||||
var rxErr cmdkit.Error
|
||||
err = dec.Decode(&rxErr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e = &rxErr
|
||||
}
|
||||
|
||||
res.SetError(e, e.Code)
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
outputType := reflect.TypeOf(req.Command().Type)
|
||||
v, err := decodeTypedVal(outputType, dec)
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res.SetOutput(v)
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// read json objects off of the given stream, and write the objects out to
|
||||
// the 'out' channel
|
||||
func readStreamedJson(req cmds.Request, rr io.Reader, out chan<- interface{}, resp cmds.Response) {
|
||||
defer close(out)
|
||||
dec := json.NewDecoder(rr)
|
||||
outputType := reflect.TypeOf(req.Command().Type)
|
||||
|
||||
ctx := req.Context()
|
||||
|
||||
for {
|
||||
v, err := decodeTypedVal(outputType, dec)
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
log.Error(err)
|
||||
resp.SetError(err, cmdkit.ErrNormal)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case out <- v:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// decode a value of the given type, if the type is nil, attempt to decode into
|
||||
// an interface{} anyways
|
||||
func decodeTypedVal(t reflect.Type, dec *json.Decoder) (interface{}, error) {
|
||||
var v interface{}
|
||||
var err error
|
||||
if t != nil {
|
||||
v = reflect.New(t).Interface()
|
||||
err = dec.Decode(v)
|
||||
} else {
|
||||
err = dec.Decode(&v)
|
||||
}
|
||||
|
||||
return v, err
|
||||
}
|
||||
|
||||
// httpResponseReader reads from the response body, and checks for an error
|
||||
// in the http trailer upon EOF, this error if present is returned instead
|
||||
// of the EOF.
|
||||
type httpResponseReader struct {
|
||||
resp *http.Response
|
||||
}
|
||||
|
||||
func (r *httpResponseReader) Read(b []byte) (int, error) {
|
||||
n, err := r.resp.Body.Read(b)
|
||||
|
||||
// reading on a closed response body is as good as an io.EOF here
|
||||
if err != nil && strings.Contains(err.Error(), "read on closed response body") {
|
||||
err = io.EOF
|
||||
}
|
||||
if err == io.EOF {
|
||||
_ = r.resp.Body.Close()
|
||||
trailerErr := r.checkError()
|
||||
if trailerErr != nil {
|
||||
return n, trailerErr
|
||||
}
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (r *httpResponseReader) checkError() error {
|
||||
if e := r.resp.Trailer.Get(StreamErrHeader); e != "" {
|
||||
return errors.New(e)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *httpResponseReader) Close() error {
|
||||
return r.resp.Body.Close()
|
||||
}
|
@ -1,466 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
cmds "github.com/ipfs/go-ipfs/commands"
|
||||
"github.com/ipfs/go-ipfs/repo/config"
|
||||
|
||||
cors "gx/ipfs/QmPG2kW5t27LuHgHnvhUwbHCNHAt2eUcb4gPHqofrESUdB/cors"
|
||||
logging "gx/ipfs/QmSpJByNKFX1sCsHBEp3R73FL4NF6FnQTEGyNAXHm2GS52/go-log"
|
||||
loggables "gx/ipfs/QmT4PgCNdv73hnFAqzHqwW44q7M9PWpykSswHDxndquZbc/go-libp2p-loggables"
|
||||
cmdkit "gx/ipfs/QmUyfy4QSr3NXym4etEiRyxBLqqAeKHJuRdi8AACxg63fZ/go-ipfs-cmdkit"
|
||||
)
|
||||
|
||||
var log = logging.Logger("commands/http")
|
||||
|
||||
// the internal handler for the API
|
||||
type internalHandler struct {
|
||||
ctx cmds.Context
|
||||
root *cmds.Command
|
||||
cfg *ServerConfig
|
||||
}
|
||||
|
||||
// The Handler struct is funny because we want to wrap our internal handler
|
||||
// with CORS while keeping our fields.
|
||||
type Handler struct {
|
||||
internalHandler
|
||||
corsHandler http.Handler
|
||||
}
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("404 page not found")
|
||||
errApiVersionMismatch = errors.New("api version mismatch")
|
||||
)
|
||||
|
||||
const (
|
||||
StreamErrHeader = "X-Stream-Error"
|
||||
streamHeader = "X-Stream-Output"
|
||||
channelHeader = "X-Chunked-Output"
|
||||
extraContentLengthHeader = "X-Content-Length"
|
||||
uaHeader = "User-Agent"
|
||||
contentTypeHeader = "Content-Type"
|
||||
applicationJson = "application/json"
|
||||
applicationOctetStream = "application/octet-stream"
|
||||
plainText = "text/plain"
|
||||
)
|
||||
|
||||
var AllowedExposedHeadersArr = []string{streamHeader, channelHeader, extraContentLengthHeader}
|
||||
var AllowedExposedHeaders = strings.Join(AllowedExposedHeadersArr, ", ")
|
||||
|
||||
const (
|
||||
ACAOrigin = "Access-Control-Allow-Origin"
|
||||
ACAMethods = "Access-Control-Allow-Methods"
|
||||
ACACredentials = "Access-Control-Allow-Credentials"
|
||||
)
|
||||
|
||||
var mimeTypes = map[string]string{
|
||||
cmds.Protobuf: "application/protobuf",
|
||||
cmds.JSON: "application/json",
|
||||
cmds.XML: "application/xml",
|
||||
cmds.Text: "text/plain",
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
// Headers is an optional map of headers that is written out.
|
||||
Headers map[string][]string
|
||||
|
||||
// cORSOpts is a set of options for CORS headers.
|
||||
cORSOpts *cors.Options
|
||||
|
||||
// cORSOptsRWMutex is a RWMutex for read/write CORSOpts
|
||||
cORSOptsRWMutex sync.RWMutex
|
||||
}
|
||||
|
||||
func skipAPIHeader(h string) bool {
|
||||
switch h {
|
||||
case "Access-Control-Allow-Origin":
|
||||
return true
|
||||
case "Access-Control-Allow-Methods":
|
||||
return true
|
||||
case "Access-Control-Allow-Credentials":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func NewHandler(ctx cmds.Context, root *cmds.Command, cfg *ServerConfig) http.Handler {
|
||||
if cfg == nil {
|
||||
panic("must provide a valid ServerConfig")
|
||||
}
|
||||
|
||||
// setup request logger
|
||||
ctx.ReqLog = new(cmds.ReqLog)
|
||||
|
||||
// Wrap the internal handler with CORS handling-middleware.
|
||||
// Create a handler for the API.
|
||||
internal := internalHandler{
|
||||
ctx: ctx,
|
||||
root: root,
|
||||
cfg: cfg,
|
||||
}
|
||||
c := cors.New(*cfg.cORSOpts)
|
||||
return &Handler{internal, c.Handler(internal)}
|
||||
}
|
||||
|
||||
func (i Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Call the CORS handler which wraps the internal handler.
|
||||
i.corsHandler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (i internalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
log.Debug("incoming API request: ", r.URL)
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Error("a panic has occurred in the commands handler!")
|
||||
log.Error(r)
|
||||
|
||||
debug.PrintStack()
|
||||
}
|
||||
}()
|
||||
|
||||
// get the node's context to pass into the commands.
|
||||
node, err := i.ctx.GetNode()
|
||||
if err != nil {
|
||||
s := fmt.Sprintf("cmds/http: couldn't GetNode(): %s", err)
|
||||
http.Error(w, s, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(node.Context())
|
||||
defer cancel()
|
||||
ctx = logging.ContextWithLoggable(ctx, loggables.Uuid("requestId"))
|
||||
if cn, ok := w.(http.CloseNotifier); ok {
|
||||
clientGone := cn.CloseNotify()
|
||||
go func() {
|
||||
select {
|
||||
case <-clientGone:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
cancel()
|
||||
}()
|
||||
}
|
||||
|
||||
if !allowOrigin(r, i.cfg) || !allowReferer(r, i.cfg) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
w.Write([]byte("403 - Forbidden"))
|
||||
log.Warningf("API blocked request to %s. (possible CSRF)", r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
req, err := Parse(r, i.root)
|
||||
if err != nil {
|
||||
if err == ErrNotFound {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
reqLogEnt := i.ctx.ReqLog.Add(req)
|
||||
defer i.ctx.ReqLog.Finish(reqLogEnt)
|
||||
|
||||
//ps: take note of the name clash - commands.Context != context.Context
|
||||
req.SetInvocContext(i.ctx)
|
||||
|
||||
err = req.SetRootContext(ctx)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// call the command
|
||||
res := i.root.Call(req)
|
||||
|
||||
// set user's headers first.
|
||||
for k, v := range i.cfg.Headers {
|
||||
if !skipAPIHeader(k) {
|
||||
w.Header()[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// now handle responding to the client properly
|
||||
sendResponse(w, r, res, req)
|
||||
}
|
||||
|
||||
func guessMimeType(res cmds.Response) (string, error) {
|
||||
// Try to guess mimeType from the encoding option
|
||||
enc, found, err := res.Request().Option(cmdkit.EncShort).String()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !found {
|
||||
return "", errors.New("no encoding option set")
|
||||
}
|
||||
|
||||
if m, ok := mimeTypes[enc]; ok {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
return mimeTypes[cmds.JSON], nil
|
||||
}
|
||||
|
||||
func sendResponse(w http.ResponseWriter, r *http.Request, res cmds.Response, req cmds.Request) {
|
||||
h := w.Header()
|
||||
// Expose our agent to allow identification
|
||||
h.Set("Server", "go-ipfs/"+config.CurrentVersionNumber)
|
||||
|
||||
mime, err := guessMimeType(res)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
status := http.StatusOK
|
||||
// if response contains an error, write an HTTP error status code
|
||||
if e := res.Error(); e != nil {
|
||||
if e.Code == cmdkit.ErrClient {
|
||||
status = http.StatusBadRequest
|
||||
} else {
|
||||
status = http.StatusInternalServerError
|
||||
}
|
||||
// NOTE: The error will actually be written out by the reader below
|
||||
}
|
||||
|
||||
out, err := res.Reader()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Set up our potential trailer
|
||||
h.Set("Trailer", StreamErrHeader)
|
||||
|
||||
if res.Length() > 0 {
|
||||
h.Set("X-Content-Length", strconv.FormatUint(res.Length(), 10))
|
||||
}
|
||||
|
||||
if _, ok := res.Output().(io.Reader); ok {
|
||||
// set streams output type to text to avoid issues with browsers rendering
|
||||
// html pages on priveleged api ports
|
||||
mime = "text/plain"
|
||||
h.Set(streamHeader, "1")
|
||||
}
|
||||
|
||||
// if output is a channel and user requested streaming channels,
|
||||
// use chunk copier for the output
|
||||
_, isChan := res.Output().(chan interface{})
|
||||
if !isChan {
|
||||
_, isChan = res.Output().(<-chan interface{})
|
||||
}
|
||||
|
||||
if isChan {
|
||||
h.Set(channelHeader, "1")
|
||||
}
|
||||
|
||||
// catch-all, set to text as default
|
||||
if mime == "" {
|
||||
mime = "text/plain"
|
||||
}
|
||||
|
||||
h.Set(contentTypeHeader, mime)
|
||||
|
||||
// set 'allowed' headers
|
||||
h.Set("Access-Control-Allow-Headers", AllowedExposedHeaders)
|
||||
// expose those headers
|
||||
h.Set("Access-Control-Expose-Headers", AllowedExposedHeaders)
|
||||
|
||||
if r.Method == "HEAD" { // after all the headers.
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(status)
|
||||
err = flushCopy(w, out)
|
||||
if err != nil {
|
||||
log.Error("err: ", err)
|
||||
w.Header().Set(StreamErrHeader, sanitizedErrStr(err))
|
||||
}
|
||||
}
|
||||
|
||||
func flushCopy(w io.Writer, r io.Reader) error {
|
||||
buf := make([]byte, 4096)
|
||||
f, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
_, err := io.Copy(w, r)
|
||||
return err
|
||||
}
|
||||
for {
|
||||
n, err := r.Read(buf)
|
||||
switch err {
|
||||
case io.EOF:
|
||||
if n <= 0 {
|
||||
return nil
|
||||
}
|
||||
// if data was returned alongside the EOF, pretend we didnt
|
||||
// get an EOF. The next read call should also EOF.
|
||||
case nil:
|
||||
// continue
|
||||
default:
|
||||
return err
|
||||
}
|
||||
|
||||
nw, err := w.Write(buf[:n])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if nw != n {
|
||||
return fmt.Errorf("http write failed to write full amount: %d != %d", nw, n)
|
||||
}
|
||||
|
||||
f.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func sanitizedErrStr(err error) string {
|
||||
s := err.Error()
|
||||
s = strings.Split(s, "\n")[0]
|
||||
s = strings.Split(s, "\r")[0]
|
||||
return s
|
||||
}
|
||||
|
||||
func NewServerConfig() *ServerConfig {
|
||||
cfg := new(ServerConfig)
|
||||
cfg.cORSOpts = new(cors.Options)
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (cfg ServerConfig) AllowedOrigins() []string {
|
||||
cfg.cORSOptsRWMutex.RLock()
|
||||
defer cfg.cORSOptsRWMutex.RUnlock()
|
||||
return cfg.cORSOpts.AllowedOrigins
|
||||
}
|
||||
|
||||
func (cfg *ServerConfig) SetAllowedOrigins(origins ...string) {
|
||||
cfg.cORSOptsRWMutex.Lock()
|
||||
defer cfg.cORSOptsRWMutex.Unlock()
|
||||
o := make([]string, len(origins))
|
||||
copy(o, origins)
|
||||
cfg.cORSOpts.AllowedOrigins = o
|
||||
}
|
||||
|
||||
func (cfg *ServerConfig) AppendAllowedOrigins(origins ...string) {
|
||||
cfg.cORSOptsRWMutex.Lock()
|
||||
defer cfg.cORSOptsRWMutex.Unlock()
|
||||
cfg.cORSOpts.AllowedOrigins = append(cfg.cORSOpts.AllowedOrigins, origins...)
|
||||
}
|
||||
|
||||
func (cfg ServerConfig) AllowedMethods() []string {
|
||||
cfg.cORSOptsRWMutex.RLock()
|
||||
defer cfg.cORSOptsRWMutex.RUnlock()
|
||||
return []string(cfg.cORSOpts.AllowedMethods)
|
||||
}
|
||||
|
||||
func (cfg *ServerConfig) SetAllowedMethods(methods ...string) {
|
||||
cfg.cORSOptsRWMutex.Lock()
|
||||
defer cfg.cORSOptsRWMutex.Unlock()
|
||||
if cfg.cORSOpts == nil {
|
||||
cfg.cORSOpts = new(cors.Options)
|
||||
}
|
||||
cfg.cORSOpts.AllowedMethods = methods
|
||||
}
|
||||
|
||||
func (cfg *ServerConfig) SetAllowCredentials(flag bool) {
|
||||
cfg.cORSOptsRWMutex.Lock()
|
||||
defer cfg.cORSOptsRWMutex.Unlock()
|
||||
cfg.cORSOpts.AllowCredentials = flag
|
||||
}
|
||||
|
||||
// allowOrigin just stops the request if the origin is not allowed.
|
||||
// the CORS middleware apparently does not do this for us...
|
||||
func allowOrigin(r *http.Request, cfg *ServerConfig) bool {
|
||||
origin := r.Header.Get("Origin")
|
||||
|
||||
// curl, or ipfs shell, typing it in manually, or clicking link
|
||||
// NOT in a browser. this opens up a hole. we should close it,
|
||||
// but right now it would break things. TODO
|
||||
if origin == "" {
|
||||
return true
|
||||
}
|
||||
origins := cfg.AllowedOrigins()
|
||||
for _, o := range origins {
|
||||
if o == "*" { // ok! you asked for it!
|
||||
return true
|
||||
}
|
||||
|
||||
if o == origin { // allowed explicitly
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// allowReferer this is here to prevent some CSRF attacks that
|
||||
// the API would be vulnerable to. We check that the Referer
|
||||
// is allowed by CORS Origin (origins and referrers here will
|
||||
// work similarly in the normla uses of the API).
|
||||
// See discussion at https://github.com/ipfs/go-ipfs/issues/1532
|
||||
func allowReferer(r *http.Request, cfg *ServerConfig) bool {
|
||||
referer := r.Referer()
|
||||
|
||||
// curl, or ipfs shell, typing it in manually, or clicking link
|
||||
// NOT in a browser. this opens up a hole. we should close it,
|
||||
// but right now it would break things. TODO
|
||||
if referer == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
u, err := url.Parse(referer)
|
||||
if err != nil {
|
||||
// bad referer. but there _is_ something, so bail.
|
||||
log.Debug("failed to parse referer: ", referer)
|
||||
// debug because referer comes straight from the client. dont want to
|
||||
// let people DOS by putting a huge referer that gets stored in log files.
|
||||
return false
|
||||
}
|
||||
origin := u.Scheme + "://" + u.Host
|
||||
|
||||
// check CORS ACAOs and pretend Referer works like an origin.
|
||||
// this is valid for many (most?) sane uses of the API in
|
||||
// other applications, and will have the desired effect.
|
||||
origins := cfg.AllowedOrigins()
|
||||
for _, o := range origins {
|
||||
if o == "*" { // ok! you asked for it!
|
||||
return true
|
||||
}
|
||||
|
||||
// referer is allowed explicitly
|
||||
if o == origin {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// apiVersionMatches checks whether the api client is running the
|
||||
// same version of go-ipfs. for now, only the exact same version of
|
||||
// client + server work. In the future, we should use semver for
|
||||
// proper API versioning! \o/
|
||||
func apiVersionMatches(r *http.Request) error {
|
||||
clientVersion := r.UserAgent()
|
||||
// skips check if client is not go-ipfs
|
||||
if clientVersion == "" || !strings.Contains(clientVersion, "/go-ipfs/") {
|
||||
return nil
|
||||
}
|
||||
|
||||
daemonVersion := config.ApiVersion
|
||||
if daemonVersion != clientVersion {
|
||||
return fmt.Errorf("%s (%s != %s)", errApiVersionMismatch, daemonVersion, clientVersion)
|
||||
}
|
||||
return nil
|
||||
}
|
@ -1,348 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
cmds "github.com/ipfs/go-ipfs/commands"
|
||||
ipfscmd "github.com/ipfs/go-ipfs/core/commands"
|
||||
coremock "github.com/ipfs/go-ipfs/core/mock"
|
||||
)
|
||||
|
||||
func assertHeaders(t *testing.T, resHeaders http.Header, reqHeaders map[string]string) {
|
||||
for name, value := range reqHeaders {
|
||||
if resHeaders.Get(name) != value {
|
||||
t.Errorf("Invalid header '%s', wanted '%s', got '%s'", name, value, resHeaders.Get(name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertStatus(t *testing.T, actual, expected int) {
|
||||
if actual != expected {
|
||||
t.Errorf("Expected status: %d got: %d", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func originCfg(origins []string) *ServerConfig {
|
||||
cfg := NewServerConfig()
|
||||
cfg.SetAllowedOrigins(origins...)
|
||||
cfg.SetAllowedMethods("GET", "PUT", "POST")
|
||||
return cfg
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
Method string
|
||||
Path string
|
||||
Code int
|
||||
Origin string
|
||||
Referer string
|
||||
AllowOrigins []string
|
||||
ReqHeaders map[string]string
|
||||
ResHeaders map[string]string
|
||||
}
|
||||
|
||||
var defaultOrigins = []string{
|
||||
"http://localhost",
|
||||
"http://127.0.0.1",
|
||||
"https://localhost",
|
||||
"https://127.0.0.1",
|
||||
}
|
||||
|
||||
func getTestServer(t *testing.T, origins []string) *httptest.Server {
|
||||
cmdsCtx, err := coremock.MockCmdsCtx()
|
||||
if err != nil {
|
||||
t.Error("failure to initialize mock cmds ctx", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
cmdRoot := &cmds.Command{
|
||||
Subcommands: map[string]*cmds.Command{
|
||||
"version": ipfscmd.VersionCmd,
|
||||
},
|
||||
}
|
||||
|
||||
if len(origins) == 0 {
|
||||
origins = defaultOrigins
|
||||
}
|
||||
|
||||
handler := NewHandler(cmdsCtx, cmdRoot, originCfg(origins))
|
||||
return httptest.NewServer(handler)
|
||||
}
|
||||
|
||||
func (tc *testCase) test(t *testing.T) {
|
||||
// defaults
|
||||
method := tc.Method
|
||||
if method == "" {
|
||||
method = "GET"
|
||||
}
|
||||
|
||||
path := tc.Path
|
||||
if path == "" {
|
||||
path = "/api/v0/version"
|
||||
}
|
||||
|
||||
expectCode := tc.Code
|
||||
if expectCode == 0 {
|
||||
expectCode = 200
|
||||
}
|
||||
|
||||
// request
|
||||
req, err := http.NewRequest(method, path, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
for k, v := range tc.ReqHeaders {
|
||||
req.Header.Add(k, v)
|
||||
}
|
||||
if tc.Origin != "" {
|
||||
req.Header.Add("Origin", tc.Origin)
|
||||
}
|
||||
if tc.Referer != "" {
|
||||
req.Header.Add("Referer", tc.Referer)
|
||||
}
|
||||
|
||||
// server
|
||||
server := getTestServer(t, tc.AllowOrigins)
|
||||
if server == nil {
|
||||
return
|
||||
}
|
||||
defer server.Close()
|
||||
|
||||
req.URL, err = url.Parse(server.URL + path)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// checks
|
||||
t.Log("GET", server.URL+path, req.Header, res.Header)
|
||||
assertHeaders(t, res.Header, tc.ResHeaders)
|
||||
assertStatus(t, res.StatusCode, expectCode)
|
||||
}
|
||||
|
||||
func TestDisallowedOrigins(t *testing.T) {
|
||||
gtc := func(origin string, allowedOrigins []string) testCase {
|
||||
return testCase{
|
||||
Origin: origin,
|
||||
AllowOrigins: allowedOrigins,
|
||||
ResHeaders: map[string]string{
|
||||
ACAOrigin: "",
|
||||
ACAMethods: "",
|
||||
ACACredentials: "",
|
||||
"Access-Control-Max-Age": "",
|
||||
"Access-Control-Expose-Headers": "",
|
||||
},
|
||||
Code: http.StatusForbidden,
|
||||
}
|
||||
}
|
||||
|
||||
tcs := []testCase{
|
||||
gtc("http://barbaz.com", nil),
|
||||
gtc("http://barbaz.com", []string{"http://localhost"}),
|
||||
gtc("http://127.0.0.1", []string{"http://localhost"}),
|
||||
gtc("http://localhost", []string{"http://127.0.0.1"}),
|
||||
gtc("http://127.0.0.1:1234", nil),
|
||||
gtc("http://localhost:1234", nil),
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
tc.test(t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowedOrigins(t *testing.T) {
|
||||
gtc := func(origin string, allowedOrigins []string) testCase {
|
||||
return testCase{
|
||||
Origin: origin,
|
||||
AllowOrigins: allowedOrigins,
|
||||
ResHeaders: map[string]string{
|
||||
ACAOrigin: origin,
|
||||
ACAMethods: "",
|
||||
ACACredentials: "",
|
||||
"Access-Control-Max-Age": "",
|
||||
"Access-Control-Expose-Headers": AllowedExposedHeaders,
|
||||
},
|
||||
Code: http.StatusOK,
|
||||
}
|
||||
}
|
||||
|
||||
tcs := []testCase{
|
||||
gtc("http://barbaz.com", []string{"http://barbaz.com", "http://localhost"}),
|
||||
gtc("http://localhost", []string{"http://barbaz.com", "http://localhost"}),
|
||||
gtc("http://localhost", nil),
|
||||
gtc("http://127.0.0.1", nil),
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
tc.test(t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWildcardOrigin(t *testing.T) {
|
||||
gtc := func(origin string, allowedOrigins []string) testCase {
|
||||
return testCase{
|
||||
Origin: origin,
|
||||
AllowOrigins: allowedOrigins,
|
||||
ResHeaders: map[string]string{
|
||||
ACAOrigin: origin,
|
||||
ACAMethods: "",
|
||||
ACACredentials: "",
|
||||
"Access-Control-Max-Age": "",
|
||||
"Access-Control-Expose-Headers": AllowedExposedHeaders,
|
||||
},
|
||||
Code: http.StatusOK,
|
||||
}
|
||||
}
|
||||
|
||||
tcs := []testCase{
|
||||
gtc("http://barbaz.com", []string{"*"}),
|
||||
gtc("http://barbaz.com", []string{"http://localhost", "*"}),
|
||||
gtc("http://127.0.0.1", []string{"http://localhost", "*"}),
|
||||
gtc("http://localhost", []string{"http://127.0.0.1", "*"}),
|
||||
gtc("http://127.0.0.1", []string{"*"}),
|
||||
gtc("http://localhost", []string{"*"}),
|
||||
gtc("http://127.0.0.1:1234", []string{"*"}),
|
||||
gtc("http://localhost:1234", []string{"*"}),
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
tc.test(t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisallowedReferer(t *testing.T) {
|
||||
gtc := func(referer string, allowedOrigins []string) testCase {
|
||||
return testCase{
|
||||
Origin: "http://localhost",
|
||||
Referer: referer,
|
||||
AllowOrigins: allowedOrigins,
|
||||
ResHeaders: map[string]string{
|
||||
ACAOrigin: "http://localhost",
|
||||
ACAMethods: "",
|
||||
ACACredentials: "",
|
||||
"Access-Control-Max-Age": "",
|
||||
"Access-Control-Expose-Headers": "",
|
||||
},
|
||||
Code: http.StatusForbidden,
|
||||
}
|
||||
}
|
||||
|
||||
tcs := []testCase{
|
||||
gtc("http://foobar.com", nil),
|
||||
gtc("http://localhost:1234", nil),
|
||||
gtc("http://127.0.0.1:1234", nil),
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
tc.test(t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowedReferer(t *testing.T) {
|
||||
gtc := func(referer string, allowedOrigins []string) testCase {
|
||||
return testCase{
|
||||
Origin: "http://localhost",
|
||||
AllowOrigins: allowedOrigins,
|
||||
ResHeaders: map[string]string{
|
||||
ACAOrigin: "http://localhost",
|
||||
ACAMethods: "",
|
||||
ACACredentials: "",
|
||||
"Access-Control-Max-Age": "",
|
||||
"Access-Control-Expose-Headers": AllowedExposedHeaders,
|
||||
},
|
||||
Code: http.StatusOK,
|
||||
}
|
||||
}
|
||||
|
||||
tcs := []testCase{
|
||||
gtc("http://barbaz.com", []string{"http://barbaz.com", "http://localhost"}),
|
||||
gtc("http://localhost", []string{"http://barbaz.com", "http://localhost"}),
|
||||
gtc("http://localhost", nil),
|
||||
gtc("http://127.0.0.1", nil),
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
tc.test(t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWildcardReferer(t *testing.T) {
|
||||
gtc := func(origin string, allowedOrigins []string) testCase {
|
||||
return testCase{
|
||||
Origin: origin,
|
||||
AllowOrigins: allowedOrigins,
|
||||
ResHeaders: map[string]string{
|
||||
ACAOrigin: origin,
|
||||
ACAMethods: "",
|
||||
ACACredentials: "",
|
||||
"Access-Control-Max-Age": "",
|
||||
"Access-Control-Expose-Headers": AllowedExposedHeaders,
|
||||
},
|
||||
Code: http.StatusOK,
|
||||
}
|
||||
}
|
||||
|
||||
tcs := []testCase{
|
||||
gtc("http://barbaz.com", []string{"*"}),
|
||||
gtc("http://barbaz.com", []string{"http://localhost", "*"}),
|
||||
gtc("http://127.0.0.1", []string{"http://localhost", "*"}),
|
||||
gtc("http://localhost", []string{"http://127.0.0.1", "*"}),
|
||||
gtc("http://127.0.0.1", []string{"*"}),
|
||||
gtc("http://localhost", []string{"*"}),
|
||||
gtc("http://127.0.0.1:1234", []string{"*"}),
|
||||
gtc("http://localhost:1234", []string{"*"}),
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
tc.test(t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowedMethod(t *testing.T) {
|
||||
gtc := func(method string, ok bool) testCase {
|
||||
code := http.StatusOK
|
||||
hdrs := map[string]string{
|
||||
ACAOrigin: "http://localhost",
|
||||
ACAMethods: method,
|
||||
ACACredentials: "",
|
||||
"Access-Control-Max-Age": "",
|
||||
"Access-Control-Expose-Headers": "",
|
||||
}
|
||||
|
||||
if !ok {
|
||||
hdrs[ACAOrigin] = ""
|
||||
hdrs[ACAMethods] = ""
|
||||
}
|
||||
|
||||
return testCase{
|
||||
Method: "OPTIONS",
|
||||
Origin: "http://localhost",
|
||||
AllowOrigins: []string{"*"},
|
||||
ReqHeaders: map[string]string{
|
||||
"Access-Control-Request-Method": method,
|
||||
},
|
||||
ResHeaders: hdrs,
|
||||
Code: code,
|
||||
}
|
||||
}
|
||||
|
||||
tcs := []testCase{
|
||||
gtc("PUT", true),
|
||||
gtc("GET", true),
|
||||
gtc("FOOBAR", false),
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
tc.test(t)
|
||||
}
|
||||
}
|
@ -1,126 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"sync"
|
||||
|
||||
files "gx/ipfs/QmUyfy4QSr3NXym4etEiRyxBLqqAeKHJuRdi8AACxg63fZ/go-ipfs-cmdkit/files"
|
||||
)
|
||||
|
||||
// MultiFileReader reads from a `commands.File` (which can be a directory of files
|
||||
// or a regular file) as HTTP multipart encoded data.
|
||||
type MultiFileReader struct {
|
||||
io.Reader
|
||||
|
||||
files []files.File
|
||||
currentFile io.Reader
|
||||
buf bytes.Buffer
|
||||
mpWriter *multipart.Writer
|
||||
closed bool
|
||||
mutex *sync.Mutex
|
||||
|
||||
// if true, the data will be type 'multipart/form-data'
|
||||
// if false, the data will be type 'multipart/mixed'
|
||||
form bool
|
||||
}
|
||||
|
||||
// NewMultiFileReader constructs a MultiFileReader. `file` can be any `commands.File`.
|
||||
// If `form` is set to true, the multipart data will have a Content-Type of 'multipart/form-data',
|
||||
// if `form` is false, the Content-Type will be 'multipart/mixed'.
|
||||
func NewMultiFileReader(file files.File, form bool) *MultiFileReader {
|
||||
mfr := &MultiFileReader{
|
||||
files: []files.File{file},
|
||||
form: form,
|
||||
mutex: &sync.Mutex{},
|
||||
}
|
||||
mfr.mpWriter = multipart.NewWriter(&mfr.buf)
|
||||
|
||||
return mfr
|
||||
}
|
||||
|
||||
func (mfr *MultiFileReader) Read(buf []byte) (written int, err error) {
|
||||
mfr.mutex.Lock()
|
||||
defer mfr.mutex.Unlock()
|
||||
|
||||
// if we are closed and the buffer is flushed, end reading
|
||||
if mfr.closed && mfr.buf.Len() == 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
// if the current file isn't set, advance to the next file
|
||||
if mfr.currentFile == nil {
|
||||
var file files.File
|
||||
for file == nil {
|
||||
if len(mfr.files) == 0 {
|
||||
mfr.mpWriter.Close()
|
||||
mfr.closed = true
|
||||
return mfr.buf.Read(buf)
|
||||
}
|
||||
|
||||
nextfile, err := mfr.files[len(mfr.files)-1].NextFile()
|
||||
if err == io.EOF {
|
||||
mfr.files = mfr.files[:len(mfr.files)-1]
|
||||
continue
|
||||
} else if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
file = nextfile
|
||||
}
|
||||
|
||||
// handle starting a new file part
|
||||
if !mfr.closed {
|
||||
|
||||
var contentType string
|
||||
if _, ok := file.(*files.Symlink); ok {
|
||||
contentType = "application/symlink"
|
||||
} else if file.IsDirectory() {
|
||||
mfr.files = append(mfr.files, file)
|
||||
contentType = "application/x-directory"
|
||||
} else {
|
||||
// otherwise, use the file as a reader to read its contents
|
||||
contentType = "application/octet-stream"
|
||||
}
|
||||
|
||||
mfr.currentFile = file
|
||||
|
||||
// write the boundary and headers
|
||||
header := make(textproto.MIMEHeader)
|
||||
filename := url.QueryEscape(file.FileName())
|
||||
header.Set("Content-Disposition", fmt.Sprintf("file; filename=\"%s\"", filename))
|
||||
|
||||
header.Set("Content-Type", contentType)
|
||||
if rf, ok := file.(*files.ReaderFile); ok {
|
||||
header.Set("abspath", rf.AbsPath())
|
||||
}
|
||||
|
||||
_, err := mfr.mpWriter.CreatePart(header)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if the buffer has something in it, read from it
|
||||
if mfr.buf.Len() > 0 {
|
||||
return mfr.buf.Read(buf)
|
||||
}
|
||||
|
||||
// otherwise, read from file data
|
||||
written, err = mfr.currentFile.Read(buf)
|
||||
if err == io.EOF {
|
||||
mfr.currentFile = nil
|
||||
return written, nil
|
||||
}
|
||||
return written, err
|
||||
}
|
||||
|
||||
// Boundary returns the boundary string to be used to separate files in the multipart data
|
||||
func (mfr *MultiFileReader) Boundary() string {
|
||||
return mfr.mpWriter.Boundary()
|
||||
}
|
@ -1,114 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gx/ipfs/QmUyfy4QSr3NXym4etEiRyxBLqqAeKHJuRdi8AACxg63fZ/go-ipfs-cmdkit/files"
|
||||
)
|
||||
|
||||
func TestOutput(t *testing.T) {
|
||||
text := "Some text! :)"
|
||||
fileset := []files.File{
|
||||
files.NewReaderFile("file.txt", "file.txt", ioutil.NopCloser(strings.NewReader(text)), nil),
|
||||
files.NewSliceFile("boop", "boop", []files.File{
|
||||
files.NewReaderFile("boop/a.txt", "boop/a.txt", ioutil.NopCloser(strings.NewReader("bleep")), nil),
|
||||
files.NewReaderFile("boop/b.txt", "boop/b.txt", ioutil.NopCloser(strings.NewReader("bloop")), nil),
|
||||
}),
|
||||
files.NewReaderFile("beep.txt", "beep.txt", ioutil.NopCloser(strings.NewReader("beep")), nil),
|
||||
}
|
||||
sf := files.NewSliceFile("", "", fileset)
|
||||
buf := make([]byte, 20)
|
||||
|
||||
// testing output by reading it with the go stdlib "mime/multipart" Reader
|
||||
mfr := NewMultiFileReader(sf, true)
|
||||
mpReader := multipart.NewReader(mfr, mfr.Boundary())
|
||||
|
||||
part, err := mpReader.NextPart()
|
||||
if part == nil || err != nil {
|
||||
t.Fatal("Expected non-nil part, nil error")
|
||||
}
|
||||
mpf, err := files.NewFileFromPart(part)
|
||||
if mpf == nil || err != nil {
|
||||
t.Fatal("Expected non-nil MultipartFile, nil error")
|
||||
}
|
||||
if mpf.IsDirectory() {
|
||||
t.Fatal("Expected file to not be a directory")
|
||||
}
|
||||
if mpf.FileName() != "file.txt" {
|
||||
t.Fatal("Expected filename to be \"file.txt\"")
|
||||
}
|
||||
if n, err := mpf.Read(buf); n != len(text) || err != nil {
|
||||
t.Fatal("Expected to read from file", n, err)
|
||||
}
|
||||
if string(buf[:len(text)]) != text {
|
||||
t.Fatal("Data read was different than expected")
|
||||
}
|
||||
|
||||
part, err = mpReader.NextPart()
|
||||
if part == nil || err != nil {
|
||||
t.Fatal("Expected non-nil part, nil error")
|
||||
}
|
||||
mpf, err = files.NewFileFromPart(part)
|
||||
if mpf == nil || err != nil {
|
||||
t.Fatal("Expected non-nil MultipartFile, nil error")
|
||||
}
|
||||
if !mpf.IsDirectory() {
|
||||
t.Fatal("Expected file to be a directory")
|
||||
}
|
||||
if mpf.FileName() != "boop" {
|
||||
t.Fatal("Expected filename to be \"boop\"")
|
||||
}
|
||||
|
||||
part, err = mpReader.NextPart()
|
||||
if part == nil || err != nil {
|
||||
t.Fatal("Expected non-nil part, nil error")
|
||||
}
|
||||
child, err := files.NewFileFromPart(part)
|
||||
if child == nil || err != nil {
|
||||
t.Fatal("Expected to be able to read a child file")
|
||||
}
|
||||
if child.IsDirectory() {
|
||||
t.Fatal("Expected file to not be a directory")
|
||||
}
|
||||
if child.FileName() != "boop/a.txt" {
|
||||
t.Fatal("Expected filename to be \"some/file/path\"")
|
||||
}
|
||||
|
||||
part, err = mpReader.NextPart()
|
||||
if part == nil || err != nil {
|
||||
t.Fatal("Expected non-nil part, nil error")
|
||||
}
|
||||
child, err = files.NewFileFromPart(part)
|
||||
if child == nil || err != nil {
|
||||
t.Fatal("Expected to be able to read a child file")
|
||||
}
|
||||
if child.IsDirectory() {
|
||||
t.Fatal("Expected file to not be a directory")
|
||||
}
|
||||
if child.FileName() != "boop/b.txt" {
|
||||
t.Fatal("Expected filename to be \"some/file/path\"")
|
||||
}
|
||||
|
||||
child, err = mpf.NextFile()
|
||||
if child != nil || err != io.EOF {
|
||||
t.Fatal("Expected to get (nil, io.EOF)")
|
||||
}
|
||||
|
||||
part, err = mpReader.NextPart()
|
||||
if part == nil || err != nil {
|
||||
t.Fatal("Expected non-nil part, nil error")
|
||||
}
|
||||
mpf, err = files.NewFileFromPart(part)
|
||||
if mpf == nil || err != nil {
|
||||
t.Fatal("Expected non-nil MultipartFile, nil error")
|
||||
}
|
||||
|
||||
part, err = mpReader.NextPart()
|
||||
if part != nil || err != io.EOF {
|
||||
t.Fatal("Expected to get (nil, io.EOF)")
|
||||
}
|
||||
}
|
@ -1,160 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"mime"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
cmds "github.com/ipfs/go-ipfs/commands"
|
||||
path "github.com/ipfs/go-ipfs/path"
|
||||
|
||||
cmdkit "gx/ipfs/QmUyfy4QSr3NXym4etEiRyxBLqqAeKHJuRdi8AACxg63fZ/go-ipfs-cmdkit"
|
||||
files "gx/ipfs/QmUyfy4QSr3NXym4etEiRyxBLqqAeKHJuRdi8AACxg63fZ/go-ipfs-cmdkit/files"
|
||||
)
|
||||
|
||||
// Parse parses the data in a http.Request and returns a command Request object
|
||||
func Parse(r *http.Request, root *cmds.Command) (cmds.Request, error) {
|
||||
if !strings.HasPrefix(r.URL.Path, ApiPath) {
|
||||
return nil, errors.New("Unexpected path prefix")
|
||||
}
|
||||
pth := path.SplitList(strings.TrimPrefix(r.URL.Path, ApiPath+"/"))
|
||||
|
||||
stringArgs := make([]string, 0)
|
||||
|
||||
if err := apiVersionMatches(r); err != nil {
|
||||
if pth[0] != "version" { // compatibility with previous version check
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
cmd, err := root.Get(pth[:len(pth)-1])
|
||||
if err != nil {
|
||||
// 404 if there is no command at that path
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
if sub := cmd.Subcommand(pth[len(pth)-1]); sub == nil {
|
||||
if len(pth) <= 1 {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
// if the last string in the path isn't a subcommand, use it as an argument
|
||||
// e.g. /objects/Qabc12345 (we are passing "Qabc12345" to the "objects" command)
|
||||
stringArgs = append(stringArgs, pth[len(pth)-1])
|
||||
pth = pth[:len(pth)-1]
|
||||
|
||||
} else {
|
||||
cmd = sub
|
||||
}
|
||||
|
||||
opts, stringArgs2 := parseOptions(r)
|
||||
stringArgs = append(stringArgs, stringArgs2...)
|
||||
|
||||
// count required argument definitions
|
||||
numRequired := 0
|
||||
for _, argDef := range cmd.Arguments {
|
||||
if argDef.Required {
|
||||
numRequired++
|
||||
}
|
||||
}
|
||||
|
||||
// count the number of provided argument values
|
||||
valCount := len(stringArgs)
|
||||
|
||||
args := make([]string, valCount)
|
||||
|
||||
valIndex := 0
|
||||
requiredFile := ""
|
||||
for _, argDef := range cmd.Arguments {
|
||||
// skip optional argument definitions if there aren't sufficient remaining values
|
||||
if valCount-valIndex <= numRequired && !argDef.Required {
|
||||
continue
|
||||
} else if argDef.Required {
|
||||
numRequired--
|
||||
}
|
||||
|
||||
if argDef.Type == cmdkit.ArgString {
|
||||
if argDef.Variadic {
|
||||
for _, s := range stringArgs {
|
||||
args[valIndex] = s
|
||||
valIndex++
|
||||
}
|
||||
valCount -= len(stringArgs)
|
||||
|
||||
} else if len(stringArgs) > 0 {
|
||||
args[valIndex] = stringArgs[0]
|
||||
stringArgs = stringArgs[1:]
|
||||
valIndex++
|
||||
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} else if argDef.Type == cmdkit.ArgFile && argDef.Required && len(requiredFile) == 0 {
|
||||
requiredFile = argDef.Name
|
||||
}
|
||||
}
|
||||
|
||||
optDefs, err := root.GetOptions(pth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// create cmds.File from multipart/form-data contents
|
||||
contentType := r.Header.Get(contentTypeHeader)
|
||||
mediatype, _, _ := mime.ParseMediaType(contentType)
|
||||
|
||||
var f files.File
|
||||
if mediatype == "multipart/form-data" {
|
||||
reader, err := r.MultipartReader()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f = &files.MultipartFile{
|
||||
Mediatype: mediatype,
|
||||
Reader: reader,
|
||||
}
|
||||
}
|
||||
|
||||
// if there is a required filearg, error if no files were provided
|
||||
if len(requiredFile) > 0 && f == nil {
|
||||
return nil, fmt.Errorf("File argument '%s' is required", requiredFile)
|
||||
}
|
||||
|
||||
req, err := cmds.NewRequest(pth, opts, args, f, cmd, optDefs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = cmd.CheckArguments(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func parseOptions(r *http.Request) (map[string]interface{}, []string) {
|
||||
opts := make(map[string]interface{})
|
||||
var args []string
|
||||
|
||||
query := r.URL.Query()
|
||||
for k, v := range query {
|
||||
if k == "arg" {
|
||||
args = v
|
||||
} else {
|
||||
opts[k] = v[0]
|
||||
}
|
||||
}
|
||||
|
||||
// default to setting encoding to JSON
|
||||
_, short := opts[cmdkit.EncShort]
|
||||
_, long := opts[cmdkit.EncLong]
|
||||
if !short && !long {
|
||||
opts[cmdkit.EncShort] = cmds.JSON
|
||||
}
|
||||
|
||||
return opts, args
|
||||
}
|
Reference in New Issue
Block a user