github.com/ooni/psiphon/tunnel-core@v0.0.0-20230105123940-fe12a24c96ee/oovendor/goptlib/args.go (about)

     1  package pt
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"sort"
     7  	"strings"
     8  )
     9  
    10  // Key–value mappings for the representation of client and server options.
    11  
    12  // Args maps a string key to a list of values. It is similar to url.Values.
    13  type Args map[string][]string
    14  
    15  // Get the first value associated with the given key. If there are any values
    16  // associated with the key, the value return has the value and ok is set to
    17  // true. If there are no values for the given key, value is "" and ok is false.
    18  // If you need access to multiple values, use the map directly.
    19  func (args Args) Get(key string) (value string, ok bool) {
    20  	if args == nil {
    21  		return "", false
    22  	}
    23  	vals, ok := args[key]
    24  	if !ok || len(vals) == 0 {
    25  		return "", false
    26  	}
    27  	return vals[0], true
    28  }
    29  
    30  // Append value to the list of values for key.
    31  func (args Args) Add(key, value string) {
    32  	args[key] = append(args[key], value)
    33  }
    34  
    35  // Return the index of the next unescaped byte in s that is in the term set, or
    36  // else the length of the string if no terminators appear. Additionally return
    37  // the unescaped string up to the returned index.
    38  func indexUnescaped(s string, term []byte) (int, string, error) {
    39  	var i int
    40  	unesc := make([]byte, 0)
    41  	for i = 0; i < len(s); i++ {
    42  		b := s[i]
    43  		// A terminator byte?
    44  		if bytes.IndexByte(term, b) != -1 {
    45  			break
    46  		}
    47  		if b == '\\' {
    48  			i++
    49  			if i >= len(s) {
    50  				return 0, "", fmt.Errorf("nothing following final escape in %q", s)
    51  			}
    52  			b = s[i]
    53  		}
    54  		unesc = append(unesc, b)
    55  	}
    56  	return i, string(unesc), nil
    57  }
    58  
    59  // Parse a name–value mapping as from an encoded SOCKS username/password.
    60  //
    61  // "If any [k=v] items are provided, they are configuration parameters for the
    62  // proxy: Tor should separate them with semicolons ... If a key or value value
    63  // must contain [an equals sign or] a semicolon or a backslash, it is escaped
    64  // with a backslash."
    65  func parseClientParameters(s string) (args Args, err error) {
    66  	args = make(Args)
    67  	if len(s) == 0 {
    68  		return
    69  	}
    70  	i := 0
    71  	for {
    72  		var key, value string
    73  		var offset, begin int
    74  
    75  		begin = i
    76  		// Read the key.
    77  		offset, key, err = indexUnescaped(s[i:], []byte{'=', ';'})
    78  		if err != nil {
    79  			return
    80  		}
    81  		i += offset
    82  		// End of string or no equals sign?
    83  		if i >= len(s) || s[i] != '=' {
    84  			err = fmt.Errorf("no equals sign in %q", s[begin:i])
    85  			return
    86  		}
    87  		// Skip the equals sign.
    88  		i++
    89  		// Read the value.
    90  		offset, value, err = indexUnescaped(s[i:], []byte{';'})
    91  		if err != nil {
    92  			return
    93  		}
    94  		i += offset
    95  		if len(key) == 0 {
    96  			err = fmt.Errorf("empty key in %q", s[begin:i])
    97  			return
    98  		}
    99  		args.Add(key, value)
   100  		if i >= len(s) {
   101  			break
   102  		}
   103  		// Skip the semicolon.
   104  		i++
   105  	}
   106  	return args, nil
   107  }
   108  
   109  // Parse a transport–name–value mapping as from TOR_PT_SERVER_TRANSPORT_OPTIONS.
   110  //
   111  // "<value> is a k=v string value with options that are to be passed to the
   112  // transport. Colons, semicolons, equal signs and backslashes must be escaped
   113  // with a backslash."
   114  // Example: trebuchet:secret=nou;trebuchet:cache=/tmp/cache;ballista:secret=yes
   115  func parseServerTransportOptions(s string) (opts map[string]Args, err error) {
   116  	opts = make(map[string]Args)
   117  	if len(s) == 0 {
   118  		return
   119  	}
   120  	i := 0
   121  	for {
   122  		var methodName, key, value string
   123  		var offset, begin int
   124  
   125  		begin = i
   126  		// Read the method name.
   127  		offset, methodName, err = indexUnescaped(s[i:], []byte{':', '=', ';'})
   128  		if err != nil {
   129  			return
   130  		}
   131  		i += offset
   132  		// End of string or no colon?
   133  		if i >= len(s) || s[i] != ':' {
   134  			err = fmt.Errorf("no colon in %q", s[begin:i])
   135  			return
   136  		}
   137  		// Skip the colon.
   138  		i++
   139  		// Read the key.
   140  		offset, key, err = indexUnescaped(s[i:], []byte{'=', ';'})
   141  		if err != nil {
   142  			return
   143  		}
   144  		i += offset
   145  		// End of string or no equals sign?
   146  		if i >= len(s) || s[i] != '=' {
   147  			err = fmt.Errorf("no equals sign in %q", s[begin:i])
   148  			return
   149  		}
   150  		// Skip the equals sign.
   151  		i++
   152  		// Read the value.
   153  		offset, value, err = indexUnescaped(s[i:], []byte{';'})
   154  		if err != nil {
   155  			return
   156  		}
   157  		i += offset
   158  		if len(methodName) == 0 {
   159  			err = fmt.Errorf("empty method name in %q", s[begin:i])
   160  			return
   161  		}
   162  		if len(key) == 0 {
   163  			err = fmt.Errorf("empty key in %q", s[begin:i])
   164  			return
   165  		}
   166  		if opts[methodName] == nil {
   167  			opts[methodName] = make(Args)
   168  		}
   169  		opts[methodName].Add(key, value)
   170  		if i >= len(s) {
   171  			break
   172  		}
   173  		// Skip the semicolon.
   174  		i++
   175  	}
   176  	return opts, nil
   177  }
   178  
   179  // Escape backslashes and all the bytes that are in set.
   180  func backslashEscape(s string, set []byte) string {
   181  	var buf bytes.Buffer
   182  	for _, b := range []byte(s) {
   183  		if b == '\\' || bytes.IndexByte(set, b) != -1 {
   184  			buf.WriteByte('\\')
   185  		}
   186  		buf.WriteByte(b)
   187  	}
   188  	return buf.String()
   189  }
   190  
   191  // Encode a name–value mapping so that it is suitable to go in the ARGS option
   192  // of an SMETHOD line. The output is sorted by key. The "ARGS:" prefix is not
   193  // added.
   194  //
   195  // "Equal signs and commas [and backslashes] must be escaped with a backslash."
   196  func encodeSmethodArgs(args Args) string {
   197  	if args == nil {
   198  		return ""
   199  	}
   200  
   201  	keys := make([]string, 0, len(args))
   202  	for key := range args {
   203  		keys = append(keys, key)
   204  	}
   205  	sort.Strings(keys)
   206  
   207  	escape := func(s string) string {
   208  		return backslashEscape(s, []byte{'=', ','})
   209  	}
   210  
   211  	var pairs []string
   212  	for _, key := range keys {
   213  		for _, value := range args[key] {
   214  			pairs = append(pairs, escape(key)+"="+escape(value))
   215  		}
   216  	}
   217  
   218  	return strings.Join(pairs, ",")
   219  }