[Pkg-privacy-commits] [golang-goptlib] 02/20: Imported Upstream version 0.1
Ximin Luo
infinity0 at moszumanska.debian.org
Sat Aug 22 10:04:06 UTC 2015
This is an automated email from the git hooks/post-receive script.
infinity0 pushed a commit to branch master
in repository golang-goptlib.
commit 208b883a959e6801f9b3d87fb43f8fb879aac58a
Author: Ximin Luo <infinity0 at pwned.gg>
Date: Thu Feb 20 23:37:02 2014 +0000
Imported Upstream version 0.1
---
.gitignore | 2 +
COPYING | 121 +++++
README | 26 ++
args.go | 220 +++++++++
args_test.go | 358 +++++++++++++++
examples/dummy-client/dummy-client.go | 140 ++++++
examples/dummy-server/dummy-server.go | 137 ++++++
pt.go | 818 ++++++++++++++++++++++++++++++++++
pt_test.go | 739 ++++++++++++++++++++++++++++++
socks.go | 243 ++++++++++
socks_test.go | 162 +++++++
11 files changed, 2966 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d4d5132
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/examples/dummy-client/dummy-client
+/examples/dummy-server/dummy-server
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..0e259d4
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,121 @@
+Creative Commons Legal Code
+
+CC0 1.0 Universal
+
+ CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+ LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
+ ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+ INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+ REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
+ PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+ THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
+ HEREUNDER.
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator
+and subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for
+the purpose of contributing to a commons of creative, cultural and
+scientific works ("Commons") that the public can reliably and without fear
+of later claims of infringement build upon, modify, incorporate in other
+works, reuse and redistribute as freely as possible in any form whatsoever
+and for any purposes, including without limitation commercial purposes.
+These owners may contribute to the Commons to promote the ideal of a free
+culture and the further production of creative, cultural and scientific
+works, or to gain reputation or greater distribution for their Work in
+part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any
+expectation of additional consideration or compensation, the person
+associating CC0 with a Work (the "Affirmer"), to the extent that he or she
+is an owner of Copyright and Related Rights in the Work, voluntarily
+elects to apply CC0 to the Work and publicly distribute the Work under its
+terms, with knowledge of his or her Copyright and Related Rights in the
+Work and the meaning and intended legal effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+protected by copyright and related or neighboring rights ("Copyright and
+Related Rights"). Copyright and Related Rights include, but are not
+limited to, the following:
+
+ i. the right to reproduce, adapt, distribute, perform, display,
+ communicate, and translate a Work;
+ ii. moral rights retained by the original author(s) and/or performer(s);
+iii. publicity and privacy rights pertaining to a person's image or
+ likeness depicted in a Work;
+ iv. rights protecting against unfair competition in regards to a Work,
+ subject to the limitations in paragraph 4(a), below;
+ v. rights protecting the extraction, dissemination, use and reuse of data
+ in a Work;
+ vi. database rights (such as those arising under Directive 96/9/EC of the
+ European Parliament and of the Council of 11 March 1996 on the legal
+ protection of databases, and under any national implementation
+ thereof, including any amended or successor version of such
+ directive); and
+vii. other similar, equivalent or corresponding rights throughout the
+ world based on applicable law or treaty, and any national
+ implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention
+of, applicable law, Affirmer hereby overtly, fully, permanently,
+irrevocably and unconditionally waives, abandons, and surrenders all of
+Affirmer's Copyright and Related Rights and associated claims and causes
+of action, whether now known or unknown (including existing as well as
+future claims and causes of action), in the Work (i) in all territories
+worldwide, (ii) for the maximum duration provided by applicable law or
+treaty (including future time extensions), (iii) in any current or future
+medium and for any number of copies, and (iv) for any purpose whatsoever,
+including without limitation commercial, advertising or promotional
+purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
+member of the public at large and to the detriment of Affirmer's heirs and
+successors, fully intending that such Waiver shall not be subject to
+revocation, rescission, cancellation, termination, or any other legal or
+equitable action to disrupt the quiet enjoyment of the Work by the public
+as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason
+be judged legally invalid or ineffective under applicable law, then the
+Waiver shall be preserved to the maximum extent permitted taking into
+account Affirmer's express Statement of Purpose. In addition, to the
+extent the Waiver is so judged Affirmer hereby grants to each affected
+person a royalty-free, non transferable, non sublicensable, non exclusive,
+irrevocable and unconditional license to exercise Affirmer's Copyright and
+Related Rights in the Work (i) in all territories worldwide, (ii) for the
+maximum duration provided by applicable law or treaty (including future
+time extensions), (iii) in any current or future medium and for any number
+of copies, and (iv) for any purpose whatsoever, including without
+limitation commercial, advertising or promotional purposes (the
+"License"). The License shall be deemed effective as of the date CC0 was
+applied by Affirmer to the Work. Should any part of the License for any
+reason be judged legally invalid or ineffective under applicable law, such
+partial invalidity or ineffectiveness shall not invalidate the remainder
+of the License, and in such case Affirmer hereby affirms that he or she
+will not (i) exercise any of his or her remaining Copyright and Related
+Rights in the Work or (ii) assert any associated claims and causes of
+action with respect to the Work, in either case contrary to Affirmer's
+express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
+ surrendered, licensed or otherwise affected by this document.
+ b. Affirmer offers the Work as-is and makes no representations or
+ warranties of any kind concerning the Work, express, implied,
+ statutory or otherwise, including without limitation warranties of
+ title, merchantability, fitness for a particular purpose, non
+ infringement, or the absence of latent or other defects, accuracy, or
+ the present or absence of errors, whether or not discoverable, all to
+ the greatest extent permissible under applicable law.
+ c. Affirmer disclaims responsibility for clearing rights of other persons
+ that may apply to the Work or any use thereof, including without
+ limitation any person's Copyright and Related Rights in the Work.
+ Further, Affirmer disclaims responsibility for obtaining any necessary
+ consents, permissions or other rights required for any use of the
+ Work.
+ d. Affirmer understands and acknowledges that Creative Commons is not a
+ party to this document and has no duty or obligation with respect to
+ this CC0 or use of the Work.
diff --git a/README b/README
new file mode 100644
index 0000000..f3ce5b3
--- /dev/null
+++ b/README
@@ -0,0 +1,26 @@
+goptlib is a library for writing Tor pluggable transports in Go.
+
+https://gitweb.torproject.org/torspec.git/blob/HEAD:/pt-spec.txt
+https://gitweb.torproject.org/torspec.git/blob/HEAD:/proposals/196-transport-control-ports.txt
+https://gitweb.torproject.org/torspec.git/blob/HEAD:/proposals/217-ext-orport-auth.txt
+
+To download a copy of the library into $GOPATH:
+ go get git.torproject.org/pluggable-transports/goptlib.git
+
+See the included example programs for examples of how to use the
+library. To build them, enter their directory and run "go build".
+ examples/dummy-client/dummy-client.go
+ examples/dummy-server/dummy-server.go
+The recommended way to start writing a new transport plugin is to copy
+dummy-client or dummy-server and make changes to it.
+
+There is browseable documentation here:
+http://godoc.org/git.torproject.org/pluggable-transports/goptlib.git
+
+Report bugs to the tor-dev at lists.torproject.org mailing list or to the
+bug tracker at https://trac.torproject.org/projects/tor.
+
+To the extent possible under law, the authors have dedicated all
+copyright and related and neighboring rights to this software to the
+public domain worldwide. This software is distributed without any
+warranty. See COPYING.
diff --git a/args.go b/args.go
new file mode 100644
index 0000000..5e96589
--- /dev/null
+++ b/args.go
@@ -0,0 +1,220 @@
+package pt
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "sort"
+ "strings"
+)
+
+// Key–value mappings for the representation of client and server options.
+
+// Args maps a string key to a list of values. It is similar to url.Values.
+type Args map[string][]string
+
+// Get the first value associated with the given key. If there are any values
+// associated with the key, the value return has the value and ok is set to
+// true. If there are no values for the given key, value is "" and ok is false.
+// If you need access to multiple values, use the map directly.
+func (args Args) Get(key string) (value string, ok bool) {
+ if args == nil {
+ return "", false
+ }
+ vals, ok := args[key]
+ if !ok || len(vals) == 0 {
+ return "", false
+ }
+ return vals[0], true
+}
+
+// Append value to the list of values for key.
+func (args Args) Add(key, value string) {
+ args[key] = append(args[key], value)
+}
+
+// Return the index of the next unescaped byte in s that is in the term set, or
+// else the length of the string if not terminators appear. Additionally return
+// the unescaped string up to the returned index.
+func indexUnescaped(s string, term []byte) (int, string, error) {
+ var i int
+ unesc := make([]byte, 0)
+ for i = 0; i < len(s); i++ {
+ b := s[i]
+ // A terminator byte?
+ if bytes.IndexByte(term, b) != -1 {
+ break
+ }
+ if b == '\\' {
+ i++
+ if i >= len(s) {
+ return 0, "", errors.New(fmt.Sprintf("nothing following final escape in %q", s))
+ }
+ b = s[i]
+ }
+ unesc = append(unesc, b)
+ }
+ return i, string(unesc), nil
+}
+
+// Parse a name–value mapping as from an encoded SOCKS username/password.
+//
+// "If any [k=v] items are provided, they are configuration parameters for the
+// proxy: Tor should separate them with semicolons ... If a key or value value
+// must contain [an equals sign or] a semicolon or a backslash, it is escaped
+// with a backslash."
+func parseClientParameters(s string) (args Args, err error) {
+ args = make(Args)
+ if len(s) == 0 {
+ return
+ }
+ i := 0
+ for {
+ var key, value string
+ var offset, begin int
+
+ begin = i
+ // Read the key.
+ offset, key, err = indexUnescaped(s[i:], []byte{'=', ';'})
+ if err != nil {
+ return
+ }
+ i += offset
+ // End of string or no equals sign?
+ if i >= len(s) || s[i] != '=' {
+ err = errors.New(fmt.Sprintf("no equals sign in %q", s[begin:i]))
+ return
+ }
+ // Skip the equals sign.
+ i++
+ // Read the value.
+ offset, value, err = indexUnescaped(s[i:], []byte{';'})
+ if err != nil {
+ return
+ }
+ i += offset
+ if len(key) == 0 {
+ err = errors.New(fmt.Sprintf("empty key in %q", s[begin:i]))
+ return
+ }
+ args.Add(key, value)
+ if i >= len(s) {
+ break
+ }
+ // Skip the semicolon.
+ i++
+ }
+ return args, nil
+}
+
+// Parse a transport–name–value mapping as from TOR_PT_SERVER_TRANSPORT_OPTIONS.
+//
+// "<value> is a k=v string value with options that are to be passed to the
+// transport. Colons, semicolons, equal signs and backslashes must be escaped
+// with a backslash."
+// Example: trebuchet:secret=nou;trebuchet:cache=/tmp/cache;ballista:secret=yes
+func parseServerTransportOptions(s string) (opts map[string]Args, err error) {
+ opts = make(map[string]Args)
+ if len(s) == 0 {
+ return
+ }
+ i := 0
+ for {
+ var methodName, key, value string
+ var offset, begin int
+
+ begin = i
+ // Read the method name.
+ offset, methodName, err = indexUnescaped(s[i:], []byte{':', '=', ';'})
+ if err != nil {
+ return
+ }
+ i += offset
+ // End of string or no colon?
+ if i >= len(s) || s[i] != ':' {
+ err = errors.New(fmt.Sprintf("no colon in %q", s[begin:i]))
+ return
+ }
+ // Skip the colon.
+ i++
+ // Read the key.
+ offset, key, err = indexUnescaped(s[i:], []byte{'=', ';'})
+ if err != nil {
+ return
+ }
+ i += offset
+ // End of string or no equals sign?
+ if i >= len(s) || s[i] != '=' {
+ err = errors.New(fmt.Sprintf("no equals sign in %q", s[begin:i]))
+ return
+ }
+ // Skip the equals sign.
+ i++
+ // Read the value.
+ offset, value, err = indexUnescaped(s[i:], []byte{';'})
+ if err != nil {
+ return
+ }
+ i += offset
+ if len(methodName) == 0 {
+ err = errors.New(fmt.Sprintf("empty method name in %q", s[begin:i]))
+ return
+ }
+ if len(key) == 0 {
+ err = errors.New(fmt.Sprintf("empty key in %q", s[begin:i]))
+ return
+ }
+ if opts[methodName] == nil {
+ opts[methodName] = make(Args)
+ }
+ opts[methodName].Add(key, value)
+ if i >= len(s) {
+ break
+ }
+ // Skip the semicolon.
+ i++
+ }
+ return opts, nil
+}
+
+// Escape backslashes and all the bytes that are in set.
+func backslashEscape(s string, set []byte) string {
+ var buf bytes.Buffer
+ for _, b := range []byte(s) {
+ if b == '\\' || bytes.IndexByte(set, b) != -1 {
+ buf.WriteByte('\\')
+ }
+ buf.WriteByte(b)
+ }
+ return buf.String()
+}
+
+// Encode a name–value mapping so that it is suitable to go in the ARGS option
+// of an SMETHOD line. The output is sorted by key. The "ARGS:" prefix is not
+// added.
+//
+// "Equal signs and commas [and backslashes] must be escaped with a backslash."
+func encodeSmethodArgs(args Args) string {
+ if args == nil {
+ return ""
+ }
+
+ keys := make([]string, 0, len(args))
+ for key, _ := range args {
+ keys = append(keys, key)
+ }
+ sort.Strings(keys)
+
+ escape := func(s string) string {
+ return backslashEscape(s, []byte{'=', ','})
+ }
+
+ var pairs []string
+ for _, key := range keys {
+ for _, value := range args[key] {
+ pairs = append(pairs, escape(key)+"="+escape(value))
+ }
+ }
+
+ return strings.Join(pairs, ",")
+}
diff --git a/args_test.go b/args_test.go
new file mode 100644
index 0000000..8a77251
--- /dev/null
+++ b/args_test.go
@@ -0,0 +1,358 @@
+package pt
+
+import (
+ "testing"
+)
+
+func stringSlicesEqual(a, b []string) bool {
+ if len(a) != len(b) {
+ return false
+ }
+ for i := range a {
+ if a[i] != b[i] {
+ return false
+ }
+ }
+ return true
+}
+
+func argsEqual(a, b Args) bool {
+ for k, av := range a {
+ bv := b[k]
+ if !stringSlicesEqual(av, bv) {
+ return false
+ }
+ }
+ for k, bv := range b {
+ av := a[k]
+ if !stringSlicesEqual(av, bv) {
+ return false
+ }
+ }
+ return true
+}
+
+func TestArgsGet(t *testing.T) {
+ args := Args{
+ "a": []string{},
+ "b": []string{"value"},
+ "c": []string{"v1", "v2", "v3"},
+ }
+ var uninit Args
+
+ var v string
+ var ok bool
+
+ // Get on nil map should be the same as Get on empty map.
+ v, ok = uninit.Get("a")
+ if !(v == "" && !ok) {
+ t.Errorf("unexpected result from Get on nil Args: %q %v", v, ok)
+ }
+
+ v, ok = args.Get("a")
+ if ok {
+ t.Errorf("Unexpected Get success for %q", "a")
+ }
+ if v != "" {
+ t.Errorf("Get failure returned other than %q: %q", "", v)
+ }
+ v, ok = args.Get("b")
+ if !ok {
+ t.Errorf("Unexpected Get failure for %q", "b")
+ }
+ if v != "value" {
+ t.Errorf("Get(%q) → %q (expected %q)", "b", v, "value")
+ }
+ v, ok = args.Get("c")
+ if !ok {
+ t.Errorf("Unexpected Get failure for %q", "c")
+ }
+ if v != "v1" {
+ t.Errorf("Get(%q) → %q (expected %q)", "c", v, "v1")
+ }
+ v, ok = args.Get("d")
+ if ok {
+ t.Errorf("Unexpected Get success for %q", "d")
+ }
+}
+
+func TestArgsAdd(t *testing.T) {
+ args := make(Args)
+ expected := Args{}
+ if !argsEqual(args, expected) {
+ t.Fatalf("%q != %q", args, expected)
+ }
+ args.Add("k1", "v1")
+ expected = Args{"k1": []string{"v1"}}
+ if !argsEqual(args, expected) {
+ t.Fatalf("%q != %q", args, expected)
+ }
+ args.Add("k2", "v2")
+ expected = Args{"k1": []string{"v1"}, "k2": []string{"v2"}}
+ if !argsEqual(args, expected) {
+ t.Fatalf("%q != %q", args, expected)
+ }
+ args.Add("k1", "v3")
+ expected = Args{"k1": []string{"v1", "v3"}, "k2": []string{"v2"}}
+ if !argsEqual(args, expected) {
+ t.Fatalf("%q != %q", args, expected)
+ }
+}
+
+func TestParseClientParameters(t *testing.T) {
+ badTests := [...]string{
+ "key",
+ "key\\",
+ "=value",
+ "==value",
+ "==key=value",
+ "key=value\\",
+ "a=b;key=value\\",
+ "a;b=c",
+ ";",
+ "key=value;",
+ ";key=value",
+ "key\\=value",
+ }
+ goodTests := [...]struct {
+ input string
+ expected Args
+ }{
+ {
+ "",
+ Args{},
+ },
+ {
+ "key=",
+ Args{"key": []string{""}},
+ },
+ {
+ "key==",
+ Args{"key": []string{"="}},
+ },
+ {
+ "key=value",
+ Args{"key": []string{"value"}},
+ },
+ {
+ "a=b=c",
+ Args{"a": []string{"b=c"}},
+ },
+ {
+ "key=a\nb",
+ Args{"key": []string{"a\nb"}},
+ },
+ {
+ "key=value\\;",
+ Args{"key": []string{"value;"}},
+ },
+ {
+ "key=\"value\"",
+ Args{"key": []string{"\"value\""}},
+ },
+ {
+ "key=\"\"value\"\"",
+ Args{"key": []string{"\"\"value\"\""}},
+ },
+ {
+ "\"key=value\"",
+ Args{"\"key": []string{"value\""}},
+ },
+ {
+ "key=value;key=value",
+ Args{"key": []string{"value", "value"}},
+ },
+ {
+ "key=value1;key=value2",
+ Args{"key": []string{"value1", "value2"}},
+ },
+ {
+ "key1=value1;key2=value2;key1=value3",
+ Args{"key1": []string{"value1", "value3"}, "key2": []string{"value2"}},
+ },
+ {
+ "\\;=\\;;\\\\=\\;",
+ Args{";": []string{";"}, "\\": []string{";"}},
+ },
+ {
+ "a\\=b=c",
+ Args{"a=b": []string{"c"}},
+ },
+ {
+ "shared-secret=rahasia;secrets-file=/tmp/blob",
+ Args{"shared-secret": []string{"rahasia"}, "secrets-file": []string{"/tmp/blob"}},
+ },
+ {
+ "rocks=20;height=5.6",
+ Args{"rocks": []string{"20"}, "height": []string{"5.6"}},
+ },
+ }
+
+ for _, input := range badTests {
+ _, err := parseClientParameters(input)
+ if err == nil {
+ t.Errorf("%q unexpectedly succeeded", input)
+ }
+ }
+
+ for _, test := range goodTests {
+ args, err := parseClientParameters(test.input)
+ if err != nil {
+ t.Errorf("%q unexpectedly returned an error: %s", test.input, err)
+ }
+ if !argsEqual(args, test.expected) {
+ t.Errorf("%q → %q (expected %q)", test.input, args, test.expected)
+ }
+ }
+}
+
+func optsEqual(a, b map[string]Args) bool {
+ for k, av := range a {
+ bv, ok := b[k]
+ if !ok || !argsEqual(av, bv) {
+ return false
+ }
+ }
+ for k, bv := range b {
+ av, ok := a[k]
+ if !ok || !argsEqual(av, bv) {
+ return false
+ }
+ }
+ return true
+}
+
+func TestParseServerTransportOptions(t *testing.T) {
+ badTests := [...]string{
+ "t\\",
+ ":=",
+ "t:=",
+ ":k=",
+ ":=v",
+ "t:=v",
+ "t:=v",
+ "t:k\\",
+ "t:k=v;",
+ "abc",
+ "t:",
+ "key=value",
+ "=value",
+ "t:k=v\\",
+ "t1:k=v;t2:k=v\\",
+ "t:=key=value",
+ "t:==key=value",
+ "t:;key=value",
+ "t:key\\=value",
+ }
+ goodTests := [...]struct {
+ input string
+ expected map[string]Args
+ }{
+ {
+ "",
+ map[string]Args{},
+ },
+ {
+ "t:k=v",
+ map[string]Args{
+ "t": Args{"k": []string{"v"}},
+ },
+ },
+ {
+ "t1:k=v1;t2:k=v2;t1:k=v3",
+ map[string]Args{
+ "t1": Args{"k": []string{"v1", "v3"}},
+ "t2": Args{"k": []string{"v2"}},
+ },
+ },
+ {
+ "t\\:1:k=v;t\\=2:k=v;t\\;3:k=v;t\\\\4:k=v",
+ map[string]Args{
+ "t:1": Args{"k": []string{"v"}},
+ "t=2": Args{"k": []string{"v"}},
+ "t;3": Args{"k": []string{"v"}},
+ "t\\4": Args{"k": []string{"v"}},
+ },
+ },
+ {
+ "t:k\\:1=v;t:k\\=2=v;t:k\\;3=v;t:k\\\\4=v",
+ map[string]Args{
+ "t": Args{
+ "k:1": []string{"v"},
+ "k=2": []string{"v"},
+ "k;3": []string{"v"},
+ "k\\4": []string{"v"},
+ },
+ },
+ },
+ {
+ "t:k=v\\:1;t:k=v\\=2;t:k=v\\;3;t:k=v\\\\4",
+ map[string]Args{
+ "t": Args{"k": []string{"v:1", "v=2", "v;3", "v\\4"}},
+ },
+ },
+ {
+ "trebuchet:secret=nou;trebuchet:cache=/tmp/cache;ballista:secret=yes",
+ map[string]Args{
+ "trebuchet": Args{"secret": []string{"nou"}, "cache": []string{"/tmp/cache"}},
+ "ballista": Args{"secret": []string{"yes"}},
+ },
+ },
+ }
+
+ for _, input := range badTests {
+ _, err := parseServerTransportOptions(input)
+ if err == nil {
+ t.Errorf("%q unexpectedly succeeded", input)
+ }
+ }
+
+ for _, test := range goodTests {
+ opts, err := parseServerTransportOptions(test.input)
+ if err != nil {
+ t.Errorf("%q unexpectedly returned an error: %s", test.input, err)
+ }
+ if !optsEqual(opts, test.expected) {
+ t.Errorf("%q → %q (expected %q)", test.input, opts, test.expected)
+ }
+ }
+}
+
+func TestEncodeSmethodArgs(t *testing.T) {
+ tests := [...]struct {
+ args Args
+ expected string
+ }{
+ {
+ nil,
+ "",
+ },
+ {
+ Args{},
+ "",
+ },
+ {
+ Args{"j": []string{"v1", "v2", "v3"}, "k": []string{"v1", "v2", "v3"}},
+ "j=v1,j=v2,j=v3,k=v1,k=v2,k=v3",
+ },
+ {
+ Args{"=,\\": []string{"=", ",", "\\"}},
+ "\\=\\,\\\\=\\=,\\=\\,\\\\=\\,,\\=\\,\\\\=\\\\",
+ },
+ {
+ Args{"secret": []string{"yes"}},
+ "secret=yes",
+ },
+ {
+ Args{"secret": []string{"nou"}, "cache": []string{"/tmp/cache"}},
+ "cache=/tmp/cache,secret=nou",
+ },
+ }
+
+ for _, test := range tests {
+ encoded := encodeSmethodArgs(test.args)
+ if encoded != test.expected {
+ t.Errorf("%q → %q (expected %q)", test.args, encoded, test.expected)
+ }
+ }
+}
diff --git a/examples/dummy-client/dummy-client.go b/examples/dummy-client/dummy-client.go
new file mode 100644
index 0000000..64f1ec1
--- /dev/null
+++ b/examples/dummy-client/dummy-client.go
@@ -0,0 +1,140 @@
+// Dummy no-op pluggable transport client. Works only as a managed proxy.
+//
+// Usage (in torrc):
+// UseBridges 1
+// Bridge dummy X.X.X.X:YYYY
+// ClientTransportPlugin dummy exec dummy-client
+//
+// Because this transport doesn't do anything to the traffic, you can use any
+// ordinary relay's ORPort in the Bridge line; it doesn't have to declare
+// support for the dummy transport.
+package main
+
+import (
+ "io"
+ "net"
+ "os"
+ "os/signal"
+ "sync"
+ "syscall"
+)
+
+import "git.torproject.org/pluggable-transports/goptlib.git"
+
+var ptInfo pt.ClientInfo
+
+// When a connection handler starts, +1 is written to this channel; when it
+// ends, -1 is written.
+var handlerChan = make(chan int)
+
+func copyLoop(a, b net.Conn) {
+ var wg sync.WaitGroup
+ wg.Add(2)
+
+ go func() {
+ io.Copy(b, a)
+ wg.Done()
+ }()
+ go func() {
+ io.Copy(a, b)
+ wg.Done()
+ }()
+
+ wg.Wait()
+}
+
+func handler(conn *pt.SocksConn) error {
+ handlerChan <- 1
+ defer func() {
+ handlerChan <- -1
+ }()
+
+ defer conn.Close()
+ remote, err := net.Dial("tcp", conn.Req.Target)
+ if err != nil {
+ conn.Reject()
+ return err
+ }
+ defer remote.Close()
+ err = conn.Grant(remote.RemoteAddr().(*net.TCPAddr))
+ if err != nil {
+ return err
+ }
+
+ copyLoop(conn, remote)
+
+ return nil
+}
+
+func acceptLoop(ln *pt.SocksListener) error {
+ defer ln.Close()
+ for {
+ conn, err := ln.AcceptSocks()
+ if err != nil {
+ if e, ok := err.(net.Error); ok && !e.Temporary() {
+ return err
+ }
+ continue
+ }
+ go handler(conn)
+ }
+}
+
+func main() {
+ var err error
+
+ ptInfo, err = pt.ClientSetup([]string{"dummy"})
+ if err != nil {
+ os.Exit(1)
+ }
+
+ listeners := make([]net.Listener, 0)
+ for _, methodName := range ptInfo.MethodNames {
+ switch methodName {
+ case "dummy":
+ ln, err := pt.ListenSocks("tcp", "127.0.0.1:0")
+ if err != nil {
+ pt.CmethodError(methodName, err.Error())
+ break
+ }
+ go acceptLoop(ln)
+ pt.Cmethod(methodName, ln.Version(), ln.Addr())
+ listeners = append(listeners, ln)
+ default:
+ pt.CmethodError(methodName, "no such method")
+ }
+ }
+ pt.CmethodsDone()
+
+ var numHandlers int = 0
+ var sig os.Signal
+ sigChan := make(chan os.Signal, 1)
+ signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
+
+ // wait for first signal
+ sig = nil
+ for sig == nil {
+ select {
+ case n := <-handlerChan:
+ numHandlers += n
+ case sig = <-sigChan:
+ }
+ }
+ for _, ln := range listeners {
+ ln.Close()
+ }
+
+ if sig == syscall.SIGTERM {
+ return
+ }
+
+ // wait for second signal or no more handlers
+ sig = nil
+ for sig == nil && numHandlers != 0 {
+ select {
+ case n := <-handlerChan:
+ numHandlers += n
+ case sig = <-sigChan:
+ }
+ }
+}
diff --git a/examples/dummy-server/dummy-server.go b/examples/dummy-server/dummy-server.go
new file mode 100644
index 0000000..ea91be9
--- /dev/null
+++ b/examples/dummy-server/dummy-server.go
@@ -0,0 +1,137 @@
+// Dummy no-op pluggable transport server. Works only as a managed proxy.
+//
+// Usage (in torrc):
+// BridgeRelay 1
+// ORPort 9001
+// ExtORPort 6669
+// ServerTransportPlugin dummy exec dummy-server
+//
+// Because the dummy transport doesn't do anything to the traffic, you can
+// connect to it with any ordinary Tor client; you don't have to use
+// dummy-client.
+package main
+
+import (
+ "io"
+ "net"
+ "os"
+ "os/signal"
+ "sync"
+ "syscall"
+)
+
+import "git.torproject.org/pluggable-transports/goptlib.git"
+
+var ptInfo pt.ServerInfo
+
+// When a connection handler starts, +1 is written to this channel; when it
+// ends, -1 is written.
+var handlerChan = make(chan int)
+
+func copyLoop(a, b net.Conn) {
+ var wg sync.WaitGroup
+ wg.Add(2)
+
+ go func() {
+ io.Copy(b, a)
+ wg.Done()
+ }()
+ go func() {
+ io.Copy(a, b)
+ wg.Done()
+ }()
+
+ wg.Wait()
+}
+
+func handler(conn net.Conn) error {
+ defer conn.Close()
+
+ handlerChan <- 1
+ defer func() {
+ handlerChan <- -1
+ }()
+
+ or, err := pt.DialOr(&ptInfo, conn.RemoteAddr().String(), "dummy")
+ if err != nil {
+ return err
+ }
+ defer or.Close()
+
+ copyLoop(conn, or)
+
+ return nil
+}
+
+func acceptLoop(ln net.Listener) error {
+ defer ln.Close()
+ for {
+ conn, err := ln.Accept()
+ if err != nil {
+ if e, ok := err.(net.Error); ok && !e.Temporary() {
+ return err
+ }
+ continue
+ }
+ go handler(conn)
+ }
+}
+
+func main() {
+ var err error
+
+ ptInfo, err = pt.ServerSetup([]string{"dummy"})
+ if err != nil {
+ os.Exit(1)
+ }
+
+ listeners := make([]net.Listener, 0)
+ for _, bindaddr := range ptInfo.Bindaddrs {
+ switch bindaddr.MethodName {
+ case "dummy":
+ ln, err := net.ListenTCP("tcp", bindaddr.Addr)
+ if err != nil {
+ pt.SmethodError(bindaddr.MethodName, err.Error())
+ break
+ }
+ go acceptLoop(ln)
+ pt.Smethod(bindaddr.MethodName, ln.Addr())
+ listeners = append(listeners, ln)
+ default:
+ pt.SmethodError(bindaddr.MethodName, "no such method")
+ }
+ }
+ pt.SmethodsDone()
+
+ var numHandlers int = 0
+ var sig os.Signal
+ sigChan := make(chan os.Signal, 1)
+ signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
+
+ // wait for first signal
+ sig = nil
+ for sig == nil {
+ select {
+ case n := <-handlerChan:
+ numHandlers += n
+ case sig = <-sigChan:
+ }
+ }
+ for _, ln := range listeners {
+ ln.Close()
+ }
+
+ if sig == syscall.SIGTERM {
+ return
+ }
+
+ // wait for second signal or no more handlers
+ sig = nil
+ for sig == nil && numHandlers != 0 {
+ select {
+ case n := <-handlerChan:
+ numHandlers += n
+ case sig = <-sigChan:
+ }
+ }
+}
diff --git a/pt.go b/pt.go
new file mode 100644
index 0000000..cb9b42f
--- /dev/null
+++ b/pt.go
@@ -0,0 +1,818 @@
+// Package pt implements the Tor pluggable transports specification.
+//
+// Sample client usage:
+// var ptInfo pt.ClientInfo
+// ...
+// func handler(conn *pt.SocksConn) error {
+// defer conn.Close()
+// remote, err := net.Dial("tcp", conn.Req.Target)
+// if err != nil {
+// conn.Reject()
+// return err
+// }
+// defer remote.Close()
+// err = conn.Grant(remote.RemoteAddr().(*net.TCPAddr))
+// if err != nil {
+// return err
+// }
+// // do something with conn and or.
+// return nil
+// }
+// func acceptLoop(ln *pt.SocksListener) error {
+// defer ln.Close()
+// for {
+// conn, err := ln.AcceptSocks()
+// if err != nil {
+// if e, ok := err.(net.Error); ok && !e.Temporary() {
+// return err
+// }
+// continue
+// }
+// go handler(conn)
+// }
+// return nil
+// }
+// ...
+// func main() {
+// var err error
+// ptInfo, err = pt.ClientSetup([]string{"foo"})
+// if err != nil {
+// os.Exit(1)
+// }
+// for _, methodName := range ptInfo.MethodNames {
+// switch methodName {
+// case "foo":
+// ln, err := pt.ListenSocks("tcp", "127.0.0.1:0")
+// if err != nil {
+// pt.CmethodError(methodName, err.Error())
+// break
+// }
+// go acceptLoop(ln)
+// pt.Cmethod(methodName, ln.Version(), ln.Addr())
+// default:
+// pt.CmethodError(methodName, "no such method")
+// }
+// }
+// pt.CmethodsDone()
+// }
+//
+// Sample server usage:
+// var ptInfo pt.ServerInfo
+// ...
+// func handler(conn net.Conn) error {
+// defer conn.Close()
+// or, err := pt.DialOr(&ptInfo, conn.RemoteAddr().String(), "foo")
+// if err != nil {
+// return
+// }
+// defer or.Close()
+// // do something with or and conn
+// return nil
+// }
+// func acceptLoop(ln net.Listener) error {
+// defer ln.Close()
+// for {
+// conn, err := ln.Accept()
+// if err != nil {
+// if e, ok := err.(net.Error); ok && !e.Temporary() {
+// return err
+// }
+// continue
+// }
+// go handler(conn)
+// }
+// return nil
+// }
+// ...
+// func main() {
+// var err error
+// ptInfo, err = pt.ServerSetup([]string{"foo"})
+// if err != nil {
+// os.Exit(1)
+// }
+// for _, bindaddr := range ptInfo.Bindaddrs {
+// switch bindaddr.MethodName {
+// case "foo":
+// ln, err := net.ListenTCP("tcp", bindaddr.Addr)
+// if err != nil {
+// pt.SmethodError(bindaddr.MethodName, err.Error())
+// break
+// }
+// go acceptLoop(ln)
+// pt.Smethod(bindaddr.MethodName, ln.Addr())
+// default:
+// pt.SmethodError(bindaddr.MethodName, "no such method")
+// }
+// }
+// pt.SmethodsDone()
+// }
+//
+// Some additional care is needed to handle SIGINT and shutdown properly. See
+// the example programs dummy-client and dummy-server.
+//
+// Tor pluggable transports specification:
+// https://gitweb.torproject.org/torspec.git/blob/HEAD:/pt-spec.txt.
+//
+// Extended ORPort:
+// https://gitweb.torproject.org/torspec.git/blob/HEAD:/proposals/196-transport-control-ports.txt.
+//
+// Extended ORPort Authentication:
+// https://gitweb.torproject.org/torspec.git/blob/HEAD:/proposals/217-ext-orport-auth.txt.
+//
+// The package implements a SOCKS4a server sufficient for a Tor client transport
+// plugin.
+//
+// http://ftp.icm.edu.pl/packages/socks/socks4/SOCKS4.protocol
+package pt
+
+import (
+ "bytes"
+ "crypto/hmac"
+ "crypto/rand"
+ "crypto/sha256"
+ "crypto/subtle"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "io"
+ "net"
+ "os"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// This type wraps a Write method and calls Sync after each Write.
+type syncWriter struct {
+ *os.File
+}
+
+// Call File.Write and then Sync. An error is returned if either operation
+// returns an error.
+func (w syncWriter) Write(p []byte) (n int, err error) {
+ n, err = w.File.Write(p)
+ if err != nil {
+ return
+ }
+ err = w.Sync()
+ return
+}
+
+// Writer to which pluggable transports negotiation messages are written. It
+// defaults to a Writer that writes to os.Stdout and calls Sync after each
+// write.
+//
+// You may, for example, log pluggable transports messages by defining a Writer
+// that logs what is written to it:
+// type logWriteWrapper struct {
+// io.Writer
+// }
+//
+// func (w logWriteWrapper) Write(p []byte) (int, error) {
+// log.Print(string(p))
+// return w.Writer.Write(p)
+// }
+// and then redefining Stdout:
+// pt.Stdout = logWriteWrapper{pt.Stdout}
+var Stdout io.Writer = syncWriter{os.Stdout}
+
+// Represents an error that can happen during negotiation, for example
+// ENV-ERROR. When an error occurs, we print it to stdout and also pass it up
+// the return chain.
+type ptErr struct {
+ Keyword string
+ Args []string
+}
+
+// Implements the error interface.
+func (err *ptErr) Error() string {
+ return formatline(err.Keyword, err.Args...)
+}
+
+func getenv(key string) string {
+ return os.Getenv(key)
+}
+
+// Returns an ENV-ERROR if the environment variable isn't set.
+func getenvRequired(key string) (string, error) {
+ value := os.Getenv(key)
+ if value == "" {
+ return "", envError(fmt.Sprintf("no %s environment variable", key))
+ }
+ return value, nil
+}
+
+// Escape a string so it contains no byte values over 127 and doesn't contain
+// any of the characters '\x00' or '\n'.
+func escape(s string) string {
+ var buf bytes.Buffer
+ for _, b := range []byte(s) {
+ if b == '\n' {
+ buf.WriteString("\\n")
+ } else if b == '\\' {
+ buf.WriteString("\\\\")
+ } else if 0 < b && b < 128 {
+ buf.WriteByte(b)
+ } else {
+ fmt.Fprintf(&buf, "\\x%02x", b)
+ }
+ }
+ return buf.String()
+}
+
+func formatline(keyword string, v ...string) string {
+ var buf bytes.Buffer
+ buf.WriteString(keyword)
+ for _, x := range v {
+ buf.WriteString(" " + escape(x))
+ }
+ return buf.String()
+}
+
+// Print a pluggable transports protocol line to Stdout. The line consists of an
+// unescaped keyword, followed by any number of escaped strings.
+func line(keyword string, v ...string) {
+ fmt.Fprintln(Stdout, formatline(keyword, v...))
+}
+
+// Emit and return the given error as a ptErr.
+func doError(keyword string, v ...string) *ptErr {
+ line(keyword, v...)
+ return &ptErr{keyword, v}
+}
+
+// Emit an ENV-ERROR line with explanation text. Returns a representation of the
+// error.
+func envError(msg string) error {
+ return doError("ENV-ERROR", msg)
+}
+
+// Emit a VERSION-ERROR line with explanation text. Returns a representation of
+// the error.
+func versionError(msg string) error {
+ return doError("VERSION-ERROR", msg)
+}
+
+// Emit a CMETHOD-ERROR line with explanation text. Returns a representation of
+// the error.
+func CmethodError(methodName, msg string) error {
+ return doError("CMETHOD-ERROR", methodName, msg)
+}
+
+// Emit an SMETHOD-ERROR line with explanation text. Returns a representation of
+// the error.
+func SmethodError(methodName, msg string) error {
+ return doError("SMETHOD-ERROR", methodName, msg)
+}
+
+// Emit a CMETHOD line. socks must be "socks4" or "socks5". Call this once for
+// each listening client SOCKS port.
+func Cmethod(name string, socks string, addr net.Addr) {
+ line("CMETHOD", name, socks, addr.String())
+}
+
+// Emit a CMETHODS DONE line. Call this after opening all client listeners.
+func CmethodsDone() {
+ line("CMETHODS", "DONE")
+}
+
+// Emit an SMETHOD line. Call this once for each listening server port.
+func Smethod(name string, addr net.Addr) {
+ line("SMETHOD", name, addr.String())
+}
+
+// Emit an SMETHOD line with an ARGS option. args is a name–value mapping that
+// will be added to the server's extrainfo document.
+//
+// This is an example of how to check for a required option:
+// secret, ok := bindaddr.Options.Get("shared-secret")
+// if ok {
+// args := pt.Args{}
+// args.Add("shared-secret", secret)
+// pt.SmethodArgs(bindaddr.MethodName, ln.Addr(), args)
+// } else {
+// pt.SmethodError(bindaddr.MethodName, "need a shared-secret option")
+// }
+// Or, if you just want to echo back the options provided by Tor from the
+// TransportServerOptions configuration,
+// pt.SmethodArgs(bindaddr.MethodName, ln.Addr(), bindaddr.Options)
+func SmethodArgs(name string, addr net.Addr, args Args) {
+ line("SMETHOD", name, addr.String(), "ARGS:"+encodeSmethodArgs(args))
+}
+
+// Emit an SMETHODS DONE line. Call this after opening all server listeners.
+func SmethodsDone() {
+ line("SMETHODS", "DONE")
+}
+
+// Get a pluggable transports version offered by Tor and understood by us, if
+// any. The only version we understand is "1". This function reads the
+// environment variable TOR_PT_MANAGED_TRANSPORT_VER.
+func getManagedTransportVer() (string, error) {
+ const transportVersion = "1"
+ managedTransportVer, err := getenvRequired("TOR_PT_MANAGED_TRANSPORT_VER")
+ if err != nil {
+ return "", err
+ }
+ for _, offered := range strings.Split(managedTransportVer, ",") {
+ if offered == transportVersion {
+ return offered, nil
+ }
+ }
+ return "", versionError("no-version")
+}
+
+// Get the intersection of the method names offered by Tor and those in
+// methodNames. This function reads the environment variable
+// TOR_PT_CLIENT_TRANSPORTS.
+func getClientTransports(star []string) ([]string, error) {
+ clientTransports, err := getenvRequired("TOR_PT_CLIENT_TRANSPORTS")
+ if err != nil {
+ return nil, err
+ }
+ if clientTransports == "*" {
+ return star, nil
+ }
+ return strings.Split(clientTransports, ","), nil
+}
+
+// This structure is returned by ClientSetup. It consists of a list of method
+// names.
+type ClientInfo struct {
+ MethodNames []string
+}
+
+// Check the client pluggable transports environment, emitting an error message
+// and returning a non-nil error if any error is encountered. star is the list
+// of method names to use in case "*" is requested by Tor. Returns a ClientInfo
+// struct.
+func ClientSetup(star []string) (info ClientInfo, err error) {
+ ver, err := getManagedTransportVer()
+ if err != nil {
+ return
+ }
+ line("VERSION", ver)
+
+ info.MethodNames, err = getClientTransports(star)
+ if err != nil {
+ return
+ }
+
+ return info, nil
+}
+
+// A combination of a method name and an address, as extracted from
+// TOR_PT_SERVER_BINDADDR.
+type Bindaddr struct {
+ MethodName string
+ Addr *net.TCPAddr
+ // Options from TOR_PT_SERVER_TRANSPORT_OPTIONS that pertain to this
+ // transport.
+ Options Args
+}
+
+func parsePort(portStr string) (int, error) {
+ port, err := strconv.ParseUint(portStr, 10, 16)
+ return int(port), err
+}
+
+// Resolve an address string into a net.TCPAddr. We are a bit more strict than
+// net.ResolveTCPAddr; we don't allow an empty host or port, and the host part
+// must be a literal IP address.
+func resolveAddr(addrStr string) (*net.TCPAddr, error) {
+ ipStr, portStr, err := net.SplitHostPort(addrStr)
+ if err != nil {
+ // Before the fixing of bug #7011, tor doesn't put brackets around IPv6
+ // addresses. Split after the last colon, assuming it is a port
+ // separator, and try adding the brackets.
+ parts := strings.Split(addrStr, ":")
+ if len(parts) <= 2 {
+ return nil, err
+ }
+ addrStr := "[" + strings.Join(parts[:len(parts)-1], ":") + "]:" + parts[len(parts)-1]
+ ipStr, portStr, err = net.SplitHostPort(addrStr)
+ }
+ if err != nil {
+ return nil, err
+ }
+ if ipStr == "" {
+ return nil, net.InvalidAddrError(fmt.Sprintf("address string %q lacks a host part", addrStr))
+ }
+ if portStr == "" {
+ return nil, net.InvalidAddrError(fmt.Sprintf("address string %q lacks a port part", addrStr))
+ }
+ ip := net.ParseIP(ipStr)
+ if ip == nil {
+ return nil, net.InvalidAddrError(fmt.Sprintf("not an IP string: %q", ipStr))
+ }
+ port, err := parsePort(portStr)
+ if err != nil {
+ return nil, err
+ }
+ return &net.TCPAddr{IP: ip, Port: port}, nil
+}
+
+// Return a new slice, the members of which are those members of addrs having a
+// MethodName in methodNames.
+func filterBindaddrs(addrs []Bindaddr, methodNames []string) []Bindaddr {
+ var result []Bindaddr
+
+ for _, ba := range addrs {
+ for _, methodName := range methodNames {
+ if ba.MethodName == methodName {
+ result = append(result, ba)
+ break
+ }
+ }
+ }
+
+ return result
+}
+
+// Return an array of Bindaddrs, being the contents of TOR_PT_SERVER_BINDADDR
+// with keys filtered by TOR_PT_SERVER_TRANSPORTS. If TOR_PT_SERVER_TRANSPORTS
+// is "*", then keys are filtered by the entries in star instead.
+// Transport-specific options from TOR_PT_SERVER_TRANSPORT_OPTIONS are assigned
+// to the Options member.
+func getServerBindaddrs(star []string) ([]Bindaddr, error) {
+ var result []Bindaddr
+
+ // Parse the list of server transport options.
+ serverTransportOptions := getenv("TOR_PT_SERVER_TRANSPORT_OPTIONS")
+ optionsMap, err := parseServerTransportOptions(serverTransportOptions)
+ if err != nil {
+ return nil, envError(fmt.Sprintf("TOR_PT_SERVER_TRANSPORT_OPTIONS: %q: %s", serverTransportOptions, err.Error()))
+ }
+
+ // Get the list of all requested bindaddrs.
+ serverBindaddr, err := getenvRequired("TOR_PT_SERVER_BINDADDR")
+ if err != nil {
+ return nil, err
+ }
+ for _, spec := range strings.Split(serverBindaddr, ",") {
+ var bindaddr Bindaddr
+
+ parts := strings.SplitN(spec, "-", 2)
+ if len(parts) != 2 {
+ return nil, envError(fmt.Sprintf("TOR_PT_SERVER_BINDADDR: %q: doesn't contain \"-\"", spec))
+ }
+ bindaddr.MethodName = parts[0]
+ addr, err := resolveAddr(parts[1])
+ if err != nil {
+ return nil, envError(fmt.Sprintf("TOR_PT_SERVER_BINDADDR: %q: %s", spec, err.Error()))
+ }
+ bindaddr.Addr = addr
+ bindaddr.Options = optionsMap[bindaddr.MethodName]
+ result = append(result, bindaddr)
+ }
+
+ // Filter by TOR_PT_SERVER_TRANSPORTS.
+ serverTransports, err := getenvRequired("TOR_PT_SERVER_TRANSPORTS")
+ if err != nil {
+ return nil, err
+ }
+ if serverTransports == "*" {
+ result = filterBindaddrs(result, star)
+ } else {
+ result = filterBindaddrs(result, strings.Split(serverTransports, ","))
+ }
+
+ return result, nil
+}
+
+func readAuthCookie(f io.Reader) ([]byte, error) {
+ authCookieHeader := []byte("! Extended ORPort Auth Cookie !\x0a")
+ buf := make([]byte, 64)
+
+ n, err := io.ReadFull(f, buf)
+ if err != nil {
+ return nil, err
+ }
+ // Check that the file ends here.
+ n, err = f.Read(make([]byte, 1))
+ if n != 0 {
+ return nil, errors.New(fmt.Sprintf("file is longer than 64 bytes"))
+ } else if err != io.EOF {
+ return nil, errors.New(fmt.Sprintf("did not find EOF at end of file"))
+ }
+ header := buf[0:32]
+ cookie := buf[32:64]
+ if subtle.ConstantTimeCompare(header, authCookieHeader) != 1 {
+ return nil, errors.New(fmt.Sprintf("missing auth cookie header"))
+ }
+
+ return cookie, nil
+}
+
+// Read and validate the contents of an auth cookie file. Returns the 32-byte
+// cookie. See section 4.2.1.2 of pt-spec.txt.
+func readAuthCookieFile(filename string) ([]byte, error) {
+ f, err := os.Open(filename)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+
+ return readAuthCookie(f)
+}
+
+// This structure is returned by ServerSetup. It consists of a list of
+// Bindaddrs, an address for the ORPort, an address for the extended ORPort (if
+// any), and an authentication cookie (if any).
+type ServerInfo struct {
+ Bindaddrs []Bindaddr
+ OrAddr *net.TCPAddr
+ ExtendedOrAddr *net.TCPAddr
+ AuthCookie []byte
+}
+
+// Check the server pluggable transports environment, emitting an error message
+// and returning a non-nil error if any error is encountered. star is the list
+// of method names to use in case "*" is requested by Tor. Resolves the various
+// requested bind addresses, the server ORPort and extended ORPort, and reads
+// the auth cookie file. Returns a ServerInfo struct.
+func ServerSetup(star []string) (info ServerInfo, err error) {
+ ver, err := getManagedTransportVer()
+ if err != nil {
+ return
+ }
+ line("VERSION", ver)
+
+ info.Bindaddrs, err = getServerBindaddrs(star)
+ if err != nil {
+ return
+ }
+
+ orPort := getenv("TOR_PT_ORPORT")
+ if orPort != "" {
+ info.OrAddr, err = resolveAddr(orPort)
+ if err != nil {
+ err = envError(fmt.Sprintf("cannot resolve TOR_PT_ORPORT %q: %s", orPort, err.Error()))
+ return
+ }
+ }
+
+ extendedOrPort := getenv("TOR_PT_EXTENDED_SERVER_PORT")
+ if extendedOrPort != "" {
+ info.ExtendedOrAddr, err = resolveAddr(extendedOrPort)
+ if err != nil {
+ err = envError(fmt.Sprintf("cannot resolve TOR_PT_EXTENDED_SERVER_PORT %q: %s", extendedOrPort, err.Error()))
+ return
+ }
+ }
+ authCookieFilename := getenv("TOR_PT_AUTH_COOKIE_FILE")
+ if authCookieFilename != "" {
+ info.AuthCookie, err = readAuthCookieFile(authCookieFilename)
+ if err != nil {
+ err = envError(fmt.Sprintf("error reading TOR_PT_AUTH_COOKIE_FILE %q: %s", authCookieFilename, err.Error()))
+ return
+ }
+ }
+
+ // Need either OrAddr or ExtendedOrAddr.
+ if info.OrAddr == nil && (info.ExtendedOrAddr == nil || info.AuthCookie == nil) {
+ err = envError("need TOR_PT_ORPORT or TOR_PT_EXTENDED_SERVER_PORT environment variable")
+ return
+ }
+
+ return info, nil
+}
+
+// See 217-ext-orport-auth.txt section 4.2.1.3.
+func computeServerHash(authCookie, clientNonce, serverNonce []byte) []byte {
+ h := hmac.New(sha256.New, authCookie)
+ io.WriteString(h, "ExtORPort authentication server-to-client hash")
+ h.Write(clientNonce)
+ h.Write(serverNonce)
+ return h.Sum([]byte{})
+}
+
+// See 217-ext-orport-auth.txt section 4.2.1.3.
+func computeClientHash(authCookie, clientNonce, serverNonce []byte) []byte {
+ h := hmac.New(sha256.New, authCookie)
+ io.WriteString(h, "ExtORPort authentication client-to-server hash")
+ h.Write(clientNonce)
+ h.Write(serverNonce)
+ return h.Sum([]byte{})
+}
+
+func extOrPortAuthenticate(s io.ReadWriter, info *ServerInfo) error {
+ // Read auth types. 217-ext-orport-auth.txt section 4.1.
+ var authTypes [256]bool
+ var count int
+ for count = 0; count < 256; count++ {
+ buf := make([]byte, 1)
+ _, err := io.ReadFull(s, buf)
+ if err != nil {
+ return err
+ }
+ b := buf[0]
+ if b == 0 {
+ break
+ }
+ authTypes[b] = true
+ }
+ if count >= 256 {
+ return errors.New(fmt.Sprintf("read 256 auth types without seeing \\x00"))
+ }
+
+ // We support only type 1, SAFE_COOKIE.
+ if !authTypes[1] {
+ return errors.New(fmt.Sprintf("server didn't offer auth type 1"))
+ }
+ _, err := s.Write([]byte{1})
+ if err != nil {
+ return err
+ }
+
+ clientNonce := make([]byte, 32)
+ clientHash := make([]byte, 32)
+ serverNonce := make([]byte, 32)
+ serverHash := make([]byte, 32)
+
+ _, err = io.ReadFull(rand.Reader, clientNonce)
+ if err != nil {
+ return err
+ }
+ _, err = s.Write(clientNonce)
+ if err != nil {
+ return err
+ }
+
+ _, err = io.ReadFull(s, serverHash)
+ if err != nil {
+ return err
+ }
+ _, err = io.ReadFull(s, serverNonce)
+ if err != nil {
+ return err
+ }
+
+ expectedServerHash := computeServerHash(info.AuthCookie, clientNonce, serverNonce)
+ if subtle.ConstantTimeCompare(serverHash, expectedServerHash) != 1 {
+ return errors.New(fmt.Sprintf("mismatch in server hash"))
+ }
+
+ clientHash = computeClientHash(info.AuthCookie, clientNonce, serverNonce)
+ _, err = s.Write(clientHash)
+ if err != nil {
+ return err
+ }
+
+ status := make([]byte, 1)
+ _, err = io.ReadFull(s, status)
+ if err != nil {
+ return err
+ }
+ if status[0] != 1 {
+ return errors.New(fmt.Sprintf("server rejected authentication"))
+ }
+
+ return nil
+}
+
+// See section 3.1 of 196-transport-control-ports.txt.
+const (
+ extOrCmdDone = 0x0000
+ extOrCmdUserAddr = 0x0001
+ extOrCmdTransport = 0x0002
+ extOrCmdOkay = 0x1000
+ extOrCmdDeny = 0x1001
+)
+
+func extOrPortSendCommand(s io.Writer, cmd uint16, body []byte) error {
+ var buf bytes.Buffer
+ if len(body) > 65535 {
+ return errors.New(fmt.Sprintf("body length %d exceeds maximum of 65535", len(body)))
+ }
+ err := binary.Write(&buf, binary.BigEndian, cmd)
+ if err != nil {
+ return err
+ }
+ err = binary.Write(&buf, binary.BigEndian, uint16(len(body)))
+ if err != nil {
+ return err
+ }
+ err = binary.Write(&buf, binary.BigEndian, body)
+ if err != nil {
+ return err
+ }
+ _, err = s.Write(buf.Bytes())
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// Send a USERADDR command on s. See section 3.1.2.1 of
+// 196-transport-control-ports.txt.
+func extOrPortSendUserAddr(s io.Writer, addr string) error {
+ return extOrPortSendCommand(s, extOrCmdUserAddr, []byte(addr))
+}
+
+// Send a TRANSPORT command on s. See section 3.1.2.2 of
+// 196-transport-control-ports.txt.
+func extOrPortSendTransport(s io.Writer, methodName string) error {
+ return extOrPortSendCommand(s, extOrCmdTransport, []byte(methodName))
+}
+
+// Send a DONE command on s. See section 3.1 of 196-transport-control-ports.txt.
+func extOrPortSendDone(s io.Writer) error {
+ return extOrPortSendCommand(s, extOrCmdDone, []byte{})
+}
+
+func extOrPortRecvCommand(s io.Reader) (cmd uint16, body []byte, err error) {
+ var bodyLen uint16
+ data := make([]byte, 4)
+
+ _, err = io.ReadFull(s, data)
+ if err != nil {
+ return
+ }
+ buf := bytes.NewBuffer(data)
+ err = binary.Read(buf, binary.BigEndian, &cmd)
+ if err != nil {
+ return
+ }
+ err = binary.Read(buf, binary.BigEndian, &bodyLen)
+ if err != nil {
+ return
+ }
+ body = make([]byte, bodyLen)
+ _, err = io.ReadFull(s, body)
+ if err != nil {
+ return
+ }
+
+ return cmd, body, err
+}
+
+// Send USERADDR and TRANSPORT commands followed by a DONE command. Wait for an
+// OKAY or DENY response command from the server. If addr or methodName is "",
+// the corresponding command is not sent. Returns nil if and only if OKAY is
+// received.
+func extOrPortSetup(s io.ReadWriter, addr, methodName string) error {
+ var err error
+
+ if addr != "" {
+ err = extOrPortSendUserAddr(s, addr)
+ if err != nil {
+ return err
+ }
+ }
+ if methodName != "" {
+ err = extOrPortSendTransport(s, methodName)
+ if err != nil {
+ return err
+ }
+ }
+ err = extOrPortSendDone(s)
+ if err != nil {
+ return err
+ }
+ cmd, _, err := extOrPortRecvCommand(s)
+ if err != nil {
+ return err
+ }
+ if cmd == extOrCmdDeny {
+ return errors.New("server returned DENY after our USERADDR and DONE")
+ } else if cmd != extOrCmdOkay {
+ return errors.New(fmt.Sprintf("server returned unknown command 0x%04x after our USERADDR and DONE", cmd))
+ }
+
+ return nil
+}
+
+// Dial info.ExtendedOrAddr if defined, or else info.OrAddr, and return an open
+// *net.TCPConn. If connecting to the extended OR port, extended OR port
+// authentication à la 217-ext-orport-auth.txt is done before returning; an
+// error is returned if authentication fails.
+//
+// The addr and methodName arguments are put in USERADDR and TRANSPORT ExtOrPort
+// commands, respectively. If either is "", the corresponding command is not
+// sent.
+func DialOr(info *ServerInfo, addr, methodName string) (*net.TCPConn, error) {
+ if info.ExtendedOrAddr == nil || info.AuthCookie == nil {
+ return net.DialTCP("tcp", nil, info.OrAddr)
+ }
+
+ s, err := net.DialTCP("tcp", nil, info.ExtendedOrAddr)
+ if err != nil {
+ return nil, err
+ }
+ s.SetDeadline(time.Now().Add(5 * time.Second))
+ err = extOrPortAuthenticate(s, info)
+ if err != nil {
+ s.Close()
+ return nil, err
+ }
+ err = extOrPortSetup(s, addr, methodName)
+ if err != nil {
+ s.Close()
+ return nil, err
+ }
+ s.SetDeadline(time.Time{})
+
+ return s, nil
+}
diff --git a/pt_test.go b/pt_test.go
new file mode 100644
index 0000000..aa3ad04
--- /dev/null
+++ b/pt_test.go
@@ -0,0 +1,739 @@
+package pt
+
+import (
+ "bytes"
+ "encoding/binary"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net"
+ "os"
+ "sort"
+ "testing"
+)
+
+func stringIsSafe(s string) bool {
+ for _, c := range []byte(s) {
+ if c == '\x00' || c == '\n' || c > 127 {
+ return false
+ }
+ }
+ return true
+}
+
+func TestEscape(t *testing.T) {
+ tests := [...]string{
+ "",
+ "abc",
+ "a\nb",
+ "a\\b",
+ "ab\\",
+ "ab\\\n",
+ "ab\n\\",
+ }
+
+ check := func(input string) {
+ output := escape(input)
+ if !stringIsSafe(output) {
+ t.Errorf("escape(%q) → %q", input, output)
+ }
+ }
+ for _, input := range tests {
+ check(input)
+ }
+ for b := 0; b < 256; b++ {
+ // check one-byte string with each byte value 0–255
+ check(string([]byte{byte(b)}))
+ // check UTF-8 encoding of each character 0–255
+ check(string(b))
+ }
+}
+
+func TestGetManagedTransportVer(t *testing.T) {
+ badTests := [...]string{
+ "",
+ "2",
+ }
+ goodTests := [...]struct {
+ input, expected string
+ }{
+ {"1", "1"},
+ {"1,1", "1"},
+ {"1,2", "1"},
+ {"2,1", "1"},
+ }
+
+ Stdout = ioutil.Discard
+
+ os.Clearenv()
+ _, err := getManagedTransportVer()
+ if err == nil {
+ t.Errorf("empty environment unexpectedly succeeded")
+ }
+
+ for _, input := range badTests {
+ os.Setenv("TOR_PT_MANAGED_TRANSPORT_VER", input)
+ _, err := getManagedTransportVer()
+ if err == nil {
+ t.Errorf("TOR_PT_MANAGED_TRANSPORT_VER=%q unexpectedly succeeded", input)
+ }
+ }
+
+ for _, test := range goodTests {
+ os.Setenv("TOR_PT_MANAGED_TRANSPORT_VER", test.input)
+ output, err := getManagedTransportVer()
+ if err != nil {
+ t.Errorf("TOR_PT_MANAGED_TRANSPORT_VER=%q unexpectedly returned an error: %s", test.input, err)
+ }
+ if output != test.expected {
+ t.Errorf("TOR_PT_MANAGED_TRANSPORT_VER=%q → %q (expected %q)", test.input, output, test.expected)
+ }
+ }
+}
+
+// return true iff the two slices contain the same elements, possibly in a
+// different order.
+func stringSetsEqual(a, b []string) bool {
+ ac := make([]string, len(a))
+ bc := make([]string, len(b))
+ copy(ac, a)
+ copy(bc, b)
+ sort.Strings(ac)
+ sort.Strings(bc)
+ if len(ac) != len(bc) {
+ return false
+ }
+ for i := 0; i < len(ac); i++ {
+ if ac[i] != bc[i] {
+ return false
+ }
+ }
+ return true
+}
+
+func tcpAddrsEqual(a, b *net.TCPAddr) bool {
+ return a.IP.Equal(b.IP) && a.Port == b.Port
+}
+
+func TestGetClientTransports(t *testing.T) {
+ tests := [...]struct {
+ ptServerClientTransports string
+ star []string
+ expected []string
+ }{
+ {
+ "*",
+ []string{},
+ []string{},
+ },
+ {
+ "*",
+ []string{"alpha", "beta", "gamma"},
+ []string{"alpha", "beta", "gamma"},
+ },
+ {
+ "alpha,beta,gamma",
+ []string{"alpha", "beta", "gamma"},
+ []string{"alpha", "beta", "gamma"},
+ },
+ {
+ "alpha,beta",
+ []string{"alpha", "beta", "gamma"},
+ []string{"alpha", "beta"},
+ },
+ {
+ "alpha",
+ []string{"alpha", "beta", "gamma"},
+ []string{"alpha"},
+ },
+ {
+ "alpha,beta,gamma",
+ []string{},
+ []string{"alpha", "beta", "gamma"},
+ },
+ {
+ "alpha,beta",
+ []string{},
+ []string{"alpha", "beta"},
+ },
+ {
+ "alpha",
+ []string{},
+ []string{"alpha"},
+ },
+ // my reading of pt-spec.txt says that "*" has special meaning
+ // only when it is the entirety of the environment variable.
+ {
+ "alpha,*,gamma",
+ []string{"alpha", "beta", "gamma"},
+ []string{"alpha", "*", "gamma"},
+ },
+ {
+ "alpha",
+ []string{"beta"},
+ []string{"alpha"},
+ },
+ }
+
+ Stdout = ioutil.Discard
+
+ os.Clearenv()
+ _, err := getClientTransports([]string{"alpha", "beta", "gamma"})
+ if err == nil {
+ t.Errorf("empty environment unexpectedly succeeded")
+ }
+
+ for _, test := range tests {
+ os.Setenv("TOR_PT_CLIENT_TRANSPORTS", test.ptServerClientTransports)
+ output, err := getClientTransports(test.star)
+ if err != nil {
+ t.Errorf("TOR_PT_CLIENT_TRANSPORTS=%q unexpectedly returned an error: %s",
+ test.ptServerClientTransports, err)
+ }
+ if !stringSetsEqual(output, test.expected) {
+ t.Errorf("TOR_PT_CLIENT_TRANSPORTS=%q %q → %q (expected %q)",
+ test.ptServerClientTransports, test.star, output, test.expected)
+ }
+ }
+}
+
+func TestResolveAddr(t *testing.T) {
+ badTests := [...]string{
+ "",
+ "1.2.3.4",
+ "1.2.3.4:",
+ "9999",
+ ":9999",
+ "[1:2::3:4]",
+ "[1:2::3:4]:",
+ "[1::2::3:4]",
+ "1:2::3:4::9999",
+ "1:2:3:4::9999",
+ "localhost:9999",
+ "[localhost]:9999",
+ "1.2.3.4:http",
+ "1.2.3.4:0x50",
+ "1.2.3.4:-65456",
+ "1.2.3.4:65536",
+ "1.2.3.4:80\x00",
+ "1.2.3.4:80 ",
+ " 1.2.3.4:80",
+ "1.2.3.4 : 80",
+ }
+ goodTests := [...]struct {
+ input string
+ expected net.TCPAddr
+ }{
+ {"1.2.3.4:9999", net.TCPAddr{IP: net.ParseIP("1.2.3.4"), Port: 9999}},
+ {"[1:2::3:4]:9999", net.TCPAddr{IP: net.ParseIP("1:2::3:4"), Port: 9999}},
+ {"1:2::3:4:9999", net.TCPAddr{IP: net.ParseIP("1:2::3:4"), Port: 9999}},
+ }
+
+ for _, input := range badTests {
+ output, err := resolveAddr(input)
+ if err == nil {
+ t.Errorf("%q unexpectedly succeeded: %q", input, output)
+ }
+ }
+
+ for _, test := range goodTests {
+ output, err := resolveAddr(test.input)
+ if err != nil {
+ t.Errorf("%q unexpectedly returned an error: %s", test.input, err)
+ }
+ if !tcpAddrsEqual(output, &test.expected) {
+ t.Errorf("%q → %q (expected %q)", test.input, output, test.expected)
+ }
+ }
+}
+
+func bindaddrSliceContains(s []Bindaddr, v Bindaddr) bool {
+ for _, sv := range s {
+ if sv.MethodName == v.MethodName && tcpAddrsEqual(sv.Addr, v.Addr) {
+ return true
+ }
+ }
+ return false
+}
+
+func bindaddrSetsEqual(a, b []Bindaddr) bool {
+ for _, v := range a {
+ if !bindaddrSliceContains(b, v) {
+ return false
+ }
+ }
+ for _, v := range b {
+ if !bindaddrSliceContains(a, v) {
+ return false
+ }
+ }
+ return true
+}
+
+func TestGetServerBindaddrs(t *testing.T) {
+ badTests := [...]struct {
+ ptServerBindaddr string
+ ptServerTransports string
+ ptServerTransportOptions string
+ star []string
+ }{
+ // bad TOR_PT_SERVER_BINDADDR
+ {
+ "alpha",
+ "alpha",
+ "",
+ []string{"alpha", "beta", "gamma"},
+ },
+ {
+ "alpha-1.2.3.4",
+ "alpha",
+ "",
+ []string{"alpha", "beta", "gamma"},
+ },
+ // missing TOR_PT_SERVER_TRANSPORTS
+ {
+ "alpha-1.2.3.4:1111",
+ "",
+ "alpha:key=value",
+ []string{"alpha"},
+ },
+ // bad TOR_PT_SERVER_TRANSPORT_OPTIONS
+ {
+ "alpha-1.2.3.4:1111",
+ "alpha",
+ "key=value",
+ []string{"alpha"},
+ },
+ }
+ goodTests := [...]struct {
+ ptServerBindaddr string
+ ptServerTransports string
+ ptServerTransportOptions string
+ star []string
+ expected []Bindaddr
+ }{
+ {
+ "alpha-1.2.3.4:1111,beta-[1:2::3:4]:2222",
+ "alpha,beta,gamma",
+ "alpha:k1=v1,beta:k2=v2,gamma:k3=v3",
+ []string{"alpha", "beta"},
+ []Bindaddr{
+ {"alpha", &net.TCPAddr{IP: net.ParseIP("1.2.3.4"), Port: 1111}, Args{"k1": []string{"v1"}}},
+ {"beta", &net.TCPAddr{IP: net.ParseIP("1:2::3:4"), Port: 2222}, Args{"k2": []string{"v2"}}},
+ },
+ },
+ {
+ "alpha-1.2.3.4:1111",
+ "xxx",
+ "",
+ []string{"alpha", "beta", "gamma"},
+ []Bindaddr{},
+ },
+ {
+ "alpha-1.2.3.4:1111",
+ "alpha,beta,gamma",
+ "",
+ []string{},
+ []Bindaddr{
+ {"alpha", &net.TCPAddr{IP: net.ParseIP("1.2.3.4"), Port: 1111}, Args{}},
+ },
+ },
+ {
+ "alpha-1.2.3.4:1111,beta-[1:2::3:4]:2222",
+ "*",
+ "",
+ []string{"alpha", "beta"},
+ []Bindaddr{
+ {"alpha", &net.TCPAddr{IP: net.ParseIP("1.2.3.4"), Port: 1111}, Args{}},
+ {"beta", &net.TCPAddr{IP: net.ParseIP("1:2::3:4"), Port: 2222}, Args{}},
+ },
+ },
+ {
+ "alpha-1.2.3.4:1111,beta-[1:2::3:4]:2222",
+ "*",
+ "",
+ []string{"alpha", "gamma"},
+ []Bindaddr{
+ {"alpha", &net.TCPAddr{IP: net.ParseIP("1.2.3.4"), Port: 1111}, Args{}},
+ },
+ },
+ {
+ "trebuchet-127.0.0.1:1984,ballista-127.0.0.1:4891",
+ "trebuchet,ballista",
+ "trebuchet:secret=nou;trebuchet:cache=/tmp/cache;ballista:secret=yes",
+ []string{"trebuchet", "ballista"},
+ []Bindaddr{
+ {"trebuchet", &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1984}, Args{"secret": []string{"nou"}, "cache": []string{"/tmp/cache"}}},
+ {"ballista", &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 4891}, Args{"secret": []string{"yes"}}},
+ },
+ },
+ }
+
+ Stdout = ioutil.Discard
+
+ os.Clearenv()
+ _, err := getServerBindaddrs([]string{"alpha", "beta", "gamma"})
+ if err == nil {
+ t.Errorf("empty environment unexpectedly succeeded")
+ }
+
+ for _, test := range badTests {
+ os.Setenv("TOR_PT_SERVER_BINDADDR", test.ptServerBindaddr)
+ os.Setenv("TOR_PT_SERVER_TRANSPORTS", test.ptServerTransports)
+ os.Setenv("TOR_PT_SERVER_TRANSPORT_OPTIONS", test.ptServerTransportOptions)
+ _, err := getServerBindaddrs(test.star)
+ if err == nil {
+ t.Errorf("TOR_PT_SERVER_BINDADDR=%q TOR_PT_SERVER_TRANSPORTS=%q TOR_PT_SERVER_TRANSPORT_OPTIONS=%q %q unexpectedly succeeded",
+ test.ptServerBindaddr, test.ptServerTransports, test.ptServerTransportOptions, test.star)
+ }
+ }
+
+ for _, test := range goodTests {
+ os.Setenv("TOR_PT_SERVER_BINDADDR", test.ptServerBindaddr)
+ os.Setenv("TOR_PT_SERVER_TRANSPORTS", test.ptServerTransports)
+ os.Setenv("TOR_PT_SERVER_TRANSPORT_OPTIONS", test.ptServerTransportOptions)
+ output, err := getServerBindaddrs(test.star)
+ if err != nil {
+ t.Errorf("TOR_PT_SERVER_BINDADDR=%q TOR_PT_SERVER_TRANSPORTS=%q TOR_PT_SERVER_TRANSPORT_OPTIONS=%q %q unexpectedly returned an error: %s",
+ test.ptServerBindaddr, test.ptServerTransports, test.ptServerTransportOptions, test.star, err)
+ }
+ if !bindaddrSetsEqual(output, test.expected) {
+ t.Errorf("TOR_PT_SERVER_BINDADDR=%q TOR_PT_SERVER_TRANSPORTS=%q TOR_PT_SERVER_TRANSPORT_OPTIONS=%q %q → %q (expected %q)",
+ test.ptServerBindaddr, test.ptServerTransports, test.ptServerTransportOptions, test.star, output, test.expected)
+ }
+ }
+}
+
+func TestReadAuthCookie(t *testing.T) {
+ badTests := [...][]byte{
+ []byte(""),
+ // bad header
+ []byte("! Impostor ORPort Auth Cookie !\x0a0123456789ABCDEF0123456789ABCDEF"),
+ // too short
+ []byte("! Extended ORPort Auth Cookie !\x0a0123456789ABCDEF0123456789ABCDE"),
+ // too long
+ []byte("! Extended ORPort Auth Cookie !\x0a0123456789ABCDEF0123456789ABCDEFX"),
+ }
+ goodTests := [...][]byte{
+ []byte("! Extended ORPort Auth Cookie !\x0a0123456789ABCDEF0123456789ABCDEF"),
+ }
+
+ for _, input := range badTests {
+ var buf bytes.Buffer
+ buf.Write(input)
+ _, err := readAuthCookie(&buf)
+ if err == nil {
+ t.Errorf("%q unexpectedly succeeded", input)
+ }
+ }
+
+ for _, input := range goodTests {
+ var buf bytes.Buffer
+ buf.Write(input)
+ cookie, err := readAuthCookie(&buf)
+ if err != nil {
+ t.Errorf("%q unexpectedly returned an error: %s", input, err)
+ }
+ if !bytes.Equal(cookie, input[32:64]) {
+ t.Errorf("%q → %q (expected %q)", input, cookie, input[:32])
+ }
+ }
+}
+
+func TestComputeServerHash(t *testing.T) {
+ authCookie := make([]byte, 32)
+ clientNonce := make([]byte, 32)
+ serverNonce := make([]byte, 32)
+ // hmac.new("\x00"*32, "ExtORPort authentication server-to-client hash" + "\x00"*64, hashlib.sha256).hexdigest()
+ expected := []byte("\x9e\x22\x19\x19\x98\x2a\x84\xf7\x5f\xaf\x60\xef\x92\x69\x49\x79\x62\x68\xc9\x78\x33\xe0\x69\x60\xff\x26\x53\x69\xa9\x0f\xd6\xd8")
+ hash := computeServerHash(authCookie, clientNonce, serverNonce)
+ if !bytes.Equal(hash, expected) {
+ t.Errorf("%x %x %x → %x (expected %x)", authCookie,
+ clientNonce, serverNonce, hash, expected)
+ }
+}
+
+func TestComputeClientHash(t *testing.T) {
+ authCookie := make([]byte, 32)
+ clientNonce := make([]byte, 32)
+ serverNonce := make([]byte, 32)
+ // hmac.new("\x00"*32, "ExtORPort authentication client-to-server hash" + "\x00"*64, hashlib.sha256).hexdigest()
+ expected := []byte("\x0f\x36\x8b\x1b\xee\x24\xaa\xbc\x54\xa9\x11\x4c\xe0\x6c\x07\x0f\x3e\xd9\x9d\x0d\x36\x8f\x59\x9c\xcc\x6d\xfd\xc8\xbf\x45\x7a\x62")
+ hash := computeClientHash(authCookie, clientNonce, serverNonce)
+ if !bytes.Equal(hash, expected) {
+ t.Errorf("%x %x %x → %x (expected %x)", authCookie,
+ clientNonce, serverNonce, hash, expected)
+ }
+}
+
+// Elide a byte slice in case it's really long.
+func fmtBytes(s []byte) string {
+ if len(s) > 100 {
+ return fmt.Sprintf("%q...(%d bytes)", s[:5], len(s))
+ } else {
+ return fmt.Sprintf("%q", s)
+ }
+}
+
+func TestExtOrSendCommand(t *testing.T) {
+ badTests := [...]struct {
+ cmd uint16
+ body []byte
+ }{
+ {0x0, make([]byte, 65536)},
+ {0x1234, make([]byte, 65536)},
+ }
+ longBody := [65535 + 2 + 2]byte{0x12, 0x34, 0xff, 0xff}
+ goodTests := [...]struct {
+ cmd uint16
+ body []byte
+ expected []byte
+ }{
+ {0x0, []byte(""), []byte("\x00\x00\x00\x00")},
+ {0x5, []byte(""), []byte("\x00\x05\x00\x00")},
+ {0xfffe, []byte(""), []byte("\xff\xfe\x00\x00")},
+ {0xffff, []byte(""), []byte("\xff\xff\x00\x00")},
+ {0x1234, []byte("hello"), []byte("\x12\x34\x00\x05hello")},
+ {0x1234, make([]byte, 65535), longBody[:]},
+ }
+
+ for _, test := range badTests {
+ var buf bytes.Buffer
+ err := extOrPortSendCommand(&buf, test.cmd, test.body)
+ if err == nil {
+ t.Errorf("0x%04x %s unexpectedly succeeded", test.cmd, fmtBytes(test.body))
+ }
+ }
+
+ for _, test := range goodTests {
+ var buf bytes.Buffer
+ err := extOrPortSendCommand(&buf, test.cmd, test.body)
+ if err != nil {
+ t.Errorf("0x%04x %s unexpectedly returned an error: %s", test.cmd, fmtBytes(test.body), err)
+ }
+ p := make([]byte, 65535+2+2)
+ n, err := buf.Read(p)
+ if err != nil {
+ t.Fatal(err)
+ }
+ output := p[:n]
+ if !bytes.Equal(output, test.expected) {
+ t.Errorf("0x%04x %s → %s (expected %s)", test.cmd, fmtBytes(test.body),
+ fmtBytes(output), fmtBytes(test.expected))
+ }
+ }
+}
+
+func TestExtOrSendUserAddr(t *testing.T) {
+ addrs := [...]string{
+ "0.0.0.0:0",
+ "1.2.3.4:9999",
+ "255.255.255.255:65535",
+ "[::]:0",
+ "[ffff:ffff:ffff:ffff:ffff:ffff:255.255.255.255]:63335",
+ }
+
+ for _, addr := range addrs {
+ var buf bytes.Buffer
+ err := extOrPortSendUserAddr(&buf, addr)
+ if err != nil {
+ t.Errorf("%s unexpectedly returned an error: %s", addr, err)
+ }
+ var cmd, length uint16
+ binary.Read(&buf, binary.BigEndian, &cmd)
+ if cmd != extOrCmdUserAddr {
+ t.Errorf("%s → cmd 0x%04x (expected 0x%04x)", addr, cmd, extOrCmdUserAddr)
+ }
+ binary.Read(&buf, binary.BigEndian, &length)
+ p := make([]byte, length+1)
+ n, err := buf.Read(p)
+ if n != int(length) {
+ t.Errorf("%s said length %d but had at least %d", addr, length, n)
+ }
+ // test that parsing the address gives something equivalent to
+ // parsing the original.
+ inputAddr, err := resolveAddr(addr)
+ if err != nil {
+ t.Fatal(err)
+ }
+ outputAddr, err := resolveAddr(string(p[:n]))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !tcpAddrsEqual(inputAddr, outputAddr) {
+ t.Errorf("%s → %s", addr, outputAddr)
+ }
+ }
+}
+
+func TestExtOrPortSendTransport(t *testing.T) {
+ tests := [...]struct {
+ methodName string
+ expected []byte
+ }{
+ {"", []byte("\x00\x02\x00\x00")},
+ {"a", []byte("\x00\x02\x00\x01a")},
+ {"alpha", []byte("\x00\x02\x00\x05alpha")},
+ }
+
+ for _, test := range tests {
+ var buf bytes.Buffer
+ err := extOrPortSendTransport(&buf, test.methodName)
+ if err != nil {
+ t.Errorf("%q unexpectedly returned an error: %s", test.methodName, err)
+ }
+ p := make([]byte, 1024)
+ n, err := buf.Read(p)
+ if err != nil {
+ t.Fatal(err)
+ }
+ output := p[:n]
+ if !bytes.Equal(output, test.expected) {
+ t.Errorf("%q → %s (expected %s)", test.methodName,
+ fmtBytes(output), fmtBytes(test.expected))
+ }
+ }
+}
+
+func TestExtOrPortSendDone(t *testing.T) {
+ expected := []byte("\x00\x00\x00\x00")
+
+ var buf bytes.Buffer
+ err := extOrPortSendDone(&buf)
+ if err != nil {
+ t.Errorf("unexpectedly returned an error: %s", err)
+ }
+ p := make([]byte, 1024)
+ n, err := buf.Read(p)
+ if err != nil {
+ t.Fatal(err)
+ }
+ output := p[:n]
+ if !bytes.Equal(output, expected) {
+ t.Errorf("→ %s (expected %s)", fmtBytes(output), fmtBytes(expected))
+ }
+}
+
+func TestExtOrPortRecvCommand(t *testing.T) {
+ badTests := [...][]byte{
+ []byte(""),
+ []byte("\x12"),
+ []byte("\x12\x34"),
+ []byte("\x12\x34\x00"),
+ []byte("\x12\x34\x00\x01"),
+ }
+ goodTests := [...]struct {
+ input []byte
+ cmd uint16
+ body []byte
+ leftover []byte
+ }{
+ {
+ []byte("\x12\x34\x00\x00"),
+ 0x1234, []byte(""), []byte(""),
+ },
+ {
+ []byte("\x12\x34\x00\x00more"),
+ 0x1234, []byte(""), []byte("more"),
+ },
+ {
+ []byte("\x12\x34\x00\x04body"),
+ 0x1234, []byte("body"), []byte(""),
+ },
+ {
+ []byte("\x12\x34\x00\x04bodymore"),
+ 0x1234, []byte("body"), []byte("more"),
+ },
+ }
+
+ for _, input := range badTests {
+ var buf bytes.Buffer
+ buf.Write(input)
+ _, _, err := extOrPortRecvCommand(&buf)
+ if err == nil {
+ t.Errorf("%q unexpectedly succeeded", fmtBytes(input))
+ }
+ }
+
+ for _, test := range goodTests {
+ var buf bytes.Buffer
+ buf.Write(test.input)
+ cmd, body, err := extOrPortRecvCommand(&buf)
+ if err != nil {
+ t.Errorf("%s unexpectedly returned an error: %s", fmtBytes(test.input), err)
+ }
+ if cmd != test.cmd {
+ t.Errorf("%s → cmd 0x%04x (expected 0x%04x)", fmtBytes(test.input), cmd, test.cmd)
+ }
+ if !bytes.Equal(body, test.body) {
+ t.Errorf("%s → body %s (expected %s)", fmtBytes(test.input),
+ fmtBytes(body), fmtBytes(test.body))
+ }
+ p := make([]byte, 1024)
+ n, err := buf.Read(p)
+ if err != nil && err != io.EOF {
+ t.Fatal(err)
+ }
+ leftover := p[:n]
+ if !bytes.Equal(leftover, test.leftover) {
+ t.Errorf("%s → leftover %s (expected %s)", fmtBytes(test.input),
+ fmtBytes(leftover), fmtBytes(test.leftover))
+ }
+ }
+}
+
+// set up so that extOrPortSetup can write to one buffer and read from another.
+type MockSetupBuf struct {
+ bytes.Buffer
+ ReadBuf bytes.Buffer
+}
+
+func (buf *MockSetupBuf) Read(p []byte) (int, error) {
+ n, err := buf.ReadBuf.Read(p)
+ return n, err
+}
+
+func testExtOrPortSetupIndividual(t *testing.T, addr, methodName string) {
+ var err error
+ var buf MockSetupBuf
+ // fake an OKAY response.
+ err = extOrPortSendCommand(&buf.ReadBuf, extOrCmdOkay, []byte{})
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = extOrPortSetup(&buf, addr, methodName)
+ if err != nil {
+ t.Fatalf("error in extOrPortSetup: %s", err)
+ }
+ for {
+ cmd, body, err := extOrPortRecvCommand(&buf.Buffer)
+ if err != nil {
+ t.Fatalf("error in extOrPortRecvCommand: %s", err)
+ }
+ if cmd == extOrCmdDone {
+ break
+ }
+ if addr != "" && cmd == extOrCmdUserAddr {
+ if string(body) != addr {
+ t.Errorf("addr=%q methodName=%q got USERADDR with body %q (expected %q)", addr, methodName, body, addr)
+ }
+ continue
+ }
+ if methodName != "" && cmd == extOrCmdTransport {
+ if string(body) != methodName {
+ t.Errorf("addr=%q methodName=%q got TRANSPORT with body %q (expected %q)", addr, methodName, body, methodName)
+ }
+ continue
+ }
+ t.Errorf("addr=%q methodName=%q got unknown cmd %d body %q", addr, methodName, cmd, body)
+ }
+}
+
+func TestExtOrPortSetup(t *testing.T) {
+ const addr = "127.0.0.1:40000"
+ const methodName = "alpha"
+ testExtOrPortSetupIndividual(t, "", "")
+ testExtOrPortSetupIndividual(t, addr, "")
+ testExtOrPortSetupIndividual(t, "", methodName)
+ testExtOrPortSetupIndividual(t, addr, methodName)
+}
diff --git a/socks.go b/socks.go
new file mode 100644
index 0000000..f34f78f
--- /dev/null
+++ b/socks.go
@@ -0,0 +1,243 @@
+package pt
+
+import (
+ "bufio"
+ "errors"
+ "fmt"
+ "io"
+ "net"
+ "time"
+)
+
+const (
+ socksVersion = 0x04
+ socksCmdConnect = 0x01
+ socksResponseVersion = 0x00
+ socksRequestGranted = 0x5a
+ socksRequestRejected = 0x5b
+)
+
+// Put a sanity timeout on how long we wait for a SOCKS request.
+const socksRequestTimeout = 5 * time.Second
+
+// SocksRequest describes a SOCKS request.
+type SocksRequest struct {
+ // The endpoint requested by the client as a "host:port" string.
+ Target string
+ // The userid string sent by the client.
+ Username string
+ // The parsed contents of Username as a key–value mapping.
+ Args Args
+}
+
+// SocksConn encapsulates a net.Conn and information associated with a SOCKS request.
+type SocksConn struct {
+ net.Conn
+ Req SocksRequest
+}
+
+// Send a message to the proxy client that access to the given address is
+// granted. If the IP field inside addr is not an IPv4 address, the IP portion
+// of the response will be four zero bytes.
+func (conn *SocksConn) Grant(addr *net.TCPAddr) error {
+ return sendSocks4aResponseGranted(conn, addr)
+}
+
+// Send a message to the proxy client that access was rejected or failed.
+func (conn *SocksConn) Reject() error {
+ return sendSocks4aResponseRejected(conn)
+}
+
+// SocksListener wraps a net.Listener in order to read a SOCKS request on Accept.
+//
+// func handleConn(conn *pt.SocksConn) error {
+// defer conn.Close()
+// remote, err := net.Dial("tcp", conn.Req.Target)
+// if err != nil {
+// conn.Reject()
+// return err
+// }
+// defer remote.Close()
+// err = conn.Grant(remote.RemoteAddr().(*net.TCPAddr))
+// if err != nil {
+// return err
+// }
+// // do something with conn and remote
+// return nil
+// }
+// ...
+// ln, err := pt.ListenSocks("tcp", "127.0.0.1:0")
+// if err != nil {
+// panic(err.Error())
+// }
+// for {
+// conn, err := ln.AcceptSocks()
+// if err != nil {
+// log.Printf("accept error: %s", err)
+// if e, ok := err.(net.Error); ok && !e.Temporary() {
+// break
+// }
+// continue
+// }
+// go handleConn(conn)
+// }
+type SocksListener struct {
+ net.Listener
+}
+
+// Open a net.Listener according to network and laddr, and return it as a
+// SocksListener.
+func ListenSocks(network, laddr string) (*SocksListener, error) {
+ ln, err := net.Listen(network, laddr)
+ if err != nil {
+ return nil, err
+ }
+ return NewSocksListener(ln), nil
+}
+
+// Create a new SocksListener wrapping the given net.Listener.
+func NewSocksListener(ln net.Listener) *SocksListener {
+ return &SocksListener{ln}
+}
+
+// Accept is the same as AcceptSocks, except that it returns a generic net.Conn.
+// It is present for the sake of satisfying the net.Listener interface.
+func (ln *SocksListener) Accept() (net.Conn, error) {
+ return ln.AcceptSocks()
+}
+
+// Call Accept on the wrapped net.Listener, do SOCKS negotiation, and return a
+// SocksConn. After accepting, you must call either conn.Grant or conn.Reject
+// (presumably after trying to connect to conn.Req.Target).
+//
+// Errors returned by AcceptSocks may be temporary (for example, EOF while
+// reading the request, or a badly formatted userid string), or permanent (e.g.,
+// the underlying socket is closed). You can determine whether an error is
+// temporary and take appropriate action with a type conversion to net.Error.
+// For example:
+//
+// for {
+// conn, err := ln.AcceptSocks()
+// if err != nil {
+// if e, ok := err.(net.Error); ok && !e.Temporary() {
+// log.Printf("permanent accept error; giving up: %s", err)
+// break
+// }
+// log.Printf("temporary accept error; trying again: %s", err)
+// continue
+// }
+// go handleConn(conn)
+// }
+func (ln *SocksListener) AcceptSocks() (*SocksConn, error) {
+ c, err := ln.Listener.Accept()
+ if err != nil {
+ return nil, err
+ }
+ conn := new(SocksConn)
+ conn.Conn = c
+ err = conn.SetDeadline(time.Now().Add(socksRequestTimeout))
+ if err != nil {
+ return nil, err
+ }
+ conn.Req, err = readSocks4aConnect(conn)
+ if err != nil {
+ conn.Close()
+ return nil, err
+ }
+ err = conn.SetDeadline(time.Time{})
+ if err != nil {
+ return nil, err
+ }
+ return conn, nil
+}
+
+// Returns "socks4", suitable to be included in a call to Cmethod.
+func (ln *SocksListener) Version() string {
+ return "socks4"
+}
+
+// Read a SOCKS4a connect request. Returns a SocksRequest.
+func readSocks4aConnect(s io.Reader) (req SocksRequest, err error) {
+ r := bufio.NewReader(s)
+
+ var h [8]byte
+ _, err = io.ReadFull(r, h[:])
+ if err != nil {
+ return
+ }
+ if h[0] != socksVersion {
+ err = errors.New(fmt.Sprintf("SOCKS header had version 0x%02x, not 0x%02x", h[0], socksVersion))
+ return
+ }
+ if h[1] != socksCmdConnect {
+ err = errors.New(fmt.Sprintf("SOCKS header had command 0x%02x, not 0x%02x", h[1], socksCmdConnect))
+ return
+ }
+
+ var usernameBytes []byte
+ usernameBytes, err = r.ReadBytes('\x00')
+ if err != nil {
+ return
+ }
+ req.Username = string(usernameBytes[:len(usernameBytes)-1])
+
+ req.Args, err = parseClientParameters(req.Username)
+ if err != nil {
+ return
+ }
+
+ var port int
+ var host string
+
+ port = int(h[2])<<8 | int(h[3])<<0
+ if h[4] == 0 && h[5] == 0 && h[6] == 0 && h[7] != 0 {
+ var hostBytes []byte
+ hostBytes, err = r.ReadBytes('\x00')
+ if err != nil {
+ return
+ }
+ host = string(hostBytes[:len(hostBytes)-1])
+ } else {
+ host = net.IPv4(h[4], h[5], h[6], h[7]).String()
+ }
+
+ if r.Buffered() != 0 {
+ err = errors.New(fmt.Sprintf("%d bytes left after SOCKS header", r.Buffered()))
+ return
+ }
+
+ req.Target = fmt.Sprintf("%s:%d", host, port)
+ return
+}
+
+// Send a SOCKS4a response with the given code and address. If the IP field
+// inside addr is not an IPv4 address, the IP portion of the response will be
+// four zero bytes.
+func sendSocks4aResponse(w io.Writer, code byte, addr *net.TCPAddr) error {
+ var resp [8]byte
+ resp[0] = socksResponseVersion
+ resp[1] = code
+ resp[2] = byte((addr.Port >> 8) & 0xff)
+ resp[3] = byte((addr.Port >> 0) & 0xff)
+ ipv4 := addr.IP.To4()
+ if ipv4 != nil {
+ resp[4] = ipv4[0]
+ resp[5] = ipv4[1]
+ resp[6] = ipv4[2]
+ resp[7] = ipv4[3]
+ }
+ _, err := w.Write(resp[:])
+ return err
+}
+
+var emptyAddr = net.TCPAddr{IP: net.IPv4(0, 0, 0, 0), Port: 0}
+
+// Send a SOCKS4a response code 0x5a.
+func sendSocks4aResponseGranted(w io.Writer, addr *net.TCPAddr) error {
+ return sendSocks4aResponse(w, socksRequestGranted, addr)
+}
+
+// Send a SOCKS4a response code 0x5b (with an all-zero address).
+func sendSocks4aResponseRejected(w io.Writer) error {
+ return sendSocks4aResponse(w, socksRequestRejected, &emptyAddr)
+}
diff --git a/socks_test.go b/socks_test.go
new file mode 100644
index 0000000..18d141a
--- /dev/null
+++ b/socks_test.go
@@ -0,0 +1,162 @@
+package pt
+
+import (
+ "bytes"
+ "net"
+ "testing"
+)
+
+func TestReadSocks4aConnect(t *testing.T) {
+ badTests := [...][]byte{
+ []byte(""),
+ // missing userid
+ []byte("\x04\x01\x12\x34\x01\x02\x03\x04"),
+ // missing \x00 after userid
+ []byte("\x04\x01\x12\x34\x01\x02\x03\x04key=value"),
+ // missing hostname
+ []byte("\x04\x01\x12\x34\x00\x00\x00\x01key=value\x00"),
+ // missing \x00 after hostname
+ []byte("\x04\x01\x12\x34\x00\x00\x00\x01key=value\x00hostname"),
+ // bad name–value mapping
+ []byte("\x04\x01\x12\x34\x00\x00\x00\x01userid\x00hostname\x00"),
+ // bad version number
+ []byte("\x03\x01\x12\x34\x01\x02\x03\x04\x00"),
+ // BIND request
+ []byte("\x04\x02\x12\x34\x01\x02\x03\x04\x00"),
+ // SOCKS5
+ []byte("\x05\x01\x00"),
+ }
+ ipTests := [...]struct {
+ input []byte
+ addr net.TCPAddr
+ userid string
+ }{
+ {
+ []byte("\x04\x01\x12\x34\x01\x02\x03\x04key=value\x00"),
+ net.TCPAddr{IP: net.ParseIP("1.2.3.4"), Port: 0x1234},
+ "key=value",
+ },
+ {
+ []byte("\x04\x01\x12\x34\x01\x02\x03\x04\x00"),
+ net.TCPAddr{IP: net.ParseIP("1.2.3.4"), Port: 0x1234},
+ "",
+ },
+ }
+ hostnameTests := [...]struct {
+ input []byte
+ target string
+ userid string
+ }{
+ {
+ []byte("\x04\x01\x12\x34\x00\x00\x00\x01key=value\x00hostname\x00"),
+ "hostname:4660",
+ "key=value",
+ },
+ {
+ []byte("\x04\x01\x12\x34\x00\x00\x00\x01\x00hostname\x00"),
+ "hostname:4660",
+ "",
+ },
+ {
+ []byte("\x04\x01\x12\x34\x00\x00\x00\x01key=value\x00\x00"),
+ ":4660",
+ "key=value",
+ },
+ {
+ []byte("\x04\x01\x12\x34\x00\x00\x00\x01\x00\x00"),
+ ":4660",
+ "",
+ },
+ }
+
+ for _, input := range badTests {
+ var buf bytes.Buffer
+ buf.Write(input)
+ _, err := readSocks4aConnect(&buf)
+ if err == nil {
+ t.Errorf("%q unexpectedly succeeded", input)
+ }
+ }
+
+ for _, test := range ipTests {
+ var buf bytes.Buffer
+ buf.Write(test.input)
+ req, err := readSocks4aConnect(&buf)
+ if err != nil {
+ t.Errorf("%q unexpectedly returned an error: %s", test.input, err)
+ }
+ addr, err := net.ResolveTCPAddr("tcp", req.Target)
+ if err != nil {
+ t.Errorf("%q → target %q: cannot resolve: %s", test.input,
+ req.Target, err)
+ }
+ if !tcpAddrsEqual(addr, &test.addr) {
+ t.Errorf("%q → address %s (expected %s)", test.input,
+ req.Target, test.addr.String())
+ }
+ if req.Username != test.userid {
+ t.Errorf("%q → username %q (expected %q)", test.input,
+ req.Username, test.userid)
+ }
+ if req.Args == nil {
+ t.Errorf("%q → unexpected nil Args from username %q", test.input, req.Username)
+ }
+ }
+
+ for _, test := range hostnameTests {
+ var buf bytes.Buffer
+ buf.Write(test.input)
+ req, err := readSocks4aConnect(&buf)
+ if err != nil {
+ t.Errorf("%q unexpectedly returned an error: %s", test.input, err)
+ }
+ if req.Target != test.target {
+ t.Errorf("%q → target %q (expected %q)", test.input,
+ req.Target, test.target)
+ }
+ if req.Username != test.userid {
+ t.Errorf("%q → username %q (expected %q)", test.input,
+ req.Username, test.userid)
+ }
+ if req.Args == nil {
+ t.Errorf("%q → unexpected nil Args from username %q", test.input, req.Username)
+ }
+ }
+}
+
+func TestSendSocks4aResponse(t *testing.T) {
+ tests := [...]struct {
+ code byte
+ addr net.TCPAddr
+ expected []byte
+ }{
+ {
+ socksRequestGranted,
+ net.TCPAddr{IP: net.ParseIP("1.2.3.4"), Port: 0x1234},
+ []byte("\x00\x5a\x12\x34\x01\x02\x03\x04"),
+ },
+ {
+ socksRequestRejected,
+ net.TCPAddr{IP: net.ParseIP("1:2::3:4"), Port: 0x1234},
+ []byte("\x00\x5b\x12\x34\x00\x00\x00\x00"),
+ },
+ }
+
+ for _, test := range tests {
+ var buf bytes.Buffer
+ err := sendSocks4aResponse(&buf, test.code, &test.addr)
+ if err != nil {
+ t.Errorf("0x%02x %s unexpectedly returned an error: %s", test.code, &test.addr, err)
+ }
+ p := make([]byte, 1024)
+ n, err := buf.Read(p)
+ if err != nil {
+ t.Fatal(err)
+ }
+ output := p[:n]
+ if !bytes.Equal(output, test.expected) {
+ t.Errorf("0x%02x %s → %v (expected %v)",
+ test.code, &test.addr, output, test.expected)
+ }
+ }
+}
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-privacy/packages/golang-goptlib.git
More information about the Pkg-privacy-commits
mailing list