diff --git a/README.md b/README.md index ceda8a4..e4a33b2 100644 --- a/README.md +++ b/README.md @@ -180,12 +180,12 @@ right protocol when setting up a connection on the client. The server can listen on multiple ports / protocols at the same time. To configure the server, you need to set up: -- [Upstreams](#channels-upstreams) +- [Upstreams](#channels) - [Severs](#servers) ##### Channels -Channels are configured in the YAML `channels` section. They define the external services that will be accessible +You may configure the channels in the YAML `channels` section. They define the external services that will be accessible through this server setup. ```yaml @@ -196,7 +196,7 @@ server: ... ``` -You may define multiple channels (upstreams). Each cannel needs the following properties: +You may define multiple channels (upstreams). Each channel needs the following properties: - `name` is the unique name given to this upstream server. This is then referenced later on in the `servers` section and on the client. A good example would be `ssh`, `web`, `oracle` etc. @@ -205,17 +205,13 @@ You may define multiple channels (upstreams). Each cannel needs the following pr ##### Servers -At this stage, the following `kinds` (protocols) are supported: `websocket`, `tcp`, `stdin` and `unix`, `unixpacket` -and `udp`. To configure the server, add it to the `servers` section of the configuration. +At this stage, the following "kinds" (protocols) are supported: `websocket`, `tcp`, `stdin` and `unix`, `unixpacket`, +`udp`, `dns+udp` and `dns+tcp`. To configure the server, add it to the `servers` section of the configuration. ```yaml server: servers: - address:
- endpoints: - - endpoint: - ... - ] [channels: [list of channels]] [caCertificate: ] [caCertificateFile: ] @@ -225,12 +221,13 @@ server: [privateKeyFile: ] [privateKeyPassword: ] [privateKeyPasswordProgram: ] + [ ... other server-specific configuration ... ] ``` Where: - `address` is the type of server and listening location. Can be `http`, `https`, `tcp`, `tcp+tls`, `stdin` - `stdin+tls`, `unix` or `unix+tls`, `udp` and `unixpacket`. + `stdin+tls`, `unix` or `unix+tls`, `udp`, `unixpacket`, `dns+udp` and `dns+tcp`. - Always use a valid url, e.g. `tcp://0.0.0.0:5000`, `https://0.0.0.0:8900`. - Address type will define the listening server style, e.g. - `http` and `https` will start an HTTP / websocket server, @@ -244,8 +241,6 @@ Where: - You can also listen on a non-secured channel (e.g. HTTP) and provide certificate info. If provided, server will support the `StartTLS` command, which executes TLS handshake after connecting. Especially useful if you're proxying the connection over an existing HTTP server. -- `endpoints` is only required or `websocket` server. It defines the list of URLs the server should listen to. - For example `/ws/all` or `/my/secret/connection`. You may listen on multiple URLs. - `channels` defines a list of upstream channels that this connection proxies. If not defined, all channels are proxied. - Define `caCertificate` or `caCertificateFile` if you want to use mutual (client and server) certificate @@ -255,17 +250,136 @@ Where: - `privateKey`, `privateKeyFile`, `privateKeyPassword` and `privateKeyPasswordProgram` should be pretty self-explanatory. They must be defined when `certificate` is set up. +###### HTTP and HTTPS (websocket) server + +Configure SocketAce to listen for HTTP or HTTPS requests. Example configuration is as follows: + +```yaml +server: + servers: + # Setup a HTTP websocket server, answering at http://192.168.1.1:8000/ws/all + - address: http://192.168.1.1:8000 + endpoints: + - endpoint: /ws/all + + # Setup a HTTP websocket server, secured by StartTLS. This allows you to proxy + # secure SocketAce connections over plain :80 HTTP connection + - address: http://192.168.1.1:8000 + endpoints: + - endpoint: /ws/all + certificateFile: cert.pem + privateKeyFile: privatekey.pem + privateKeyPassword: test1234 + + # Setup a HTTPS websocket server, answering at http://192.168.1.1:8443/ws/ssh + - address: http://192.168.1.1:8000 + endpoints: + - channels: [ 'ssh' ] + endpoint: /ws/ssh + certificateFile: cert.pem + privateKeyFile: privatekey.pem + privateKeyPassword: test1234 +``` + +Additional options are as follows: +- `endpoints` defines the list of URLs the server should listen to. + For example `/ws/all` or `/my/secret/connection`. You may listen on multiple URLs. + +###### TCP socket and TLS socket server + +Configure SocketAce to listen on an unecrypted or encrypted socket. Example configuration is as follows: + +```yaml +server: + servers: + # Simple socket proxy. No security. Expose all channels. + - address: tcp://192.168.1.1:9000 + # Simple socket proxy. Secure by directly encrypting the socket. + - address: tcp+tls://192.168.1.1:9443 + certificateFile: cert.pem + privateKeyFile: privatekey.pem + privateKeyPassword: test1234 +``` + +TCP and TLS sockets require no additional options. + +###### UDP socket server + +Configure SocketAce to listen on an unecrypted UDP socket. Example configuration is as follows: + +```yaml +server: + servers: + # Simple UDP proxy. Secured by StartTLS. + - address: udp://127.0.0.1:9992 + certificateFile: cert.pem + privateKeyFile: privatekey.pem + privateKeyPassword: test1234 +``` + +###### Standard input/output server + +SokcetAce can also listen on standard input/output. This allows you to carry the SocketAce connection +over alternative means (e.g. via SSH, TELNET or serial ports). As long as you can then pipe it to a +standard input / output, you're good to go. *Notice that this option may be used only once.* + +```yaml +server: + servers: + # Simple socket proxy listening on stdin/stdout. + - address: "stdin://" +``` + +```yaml +server: + servers: + # Simple socket proxy listening on stdin/stdout. Secured by StartTLS. + - address: "stdin://" + certificateFile: cert.pem + privateKeyFile: privatekey.pem + privateKeyPassword: test1234 +``` + +###### DNS server + +SocketAce may be proxied over DNS server. It works similar to [iodine](https://github.com/yarrick/iodine) (in fact, +much of the code was referenced from there) but carries a SocketAce connection instead. Gone is the shared-secret +password and SocketAce security is used in stead. *Note that it might be a good idea to use mutual TLS authentication +with public DNS servers.* + +```yaml +server: + servers: + # UDP DNS server for SocketAce-over-DNS Secured by StartTLS. + - address: "dns+udp://192.168.8.1:53" + domain: "example.org" + certificateFile: cert.pem + privateKeyFile: privatekey.pem + privateKeyPassword: test1234 + - address: "dns+tcp://192.168.8.1:53" + domain: "example.org" + certificateFile: cert.pem + privateKeyFile: privatekey.pem + privateKeyPassword: test1234 +``` + +The `domain` represents the listening domain. You will need to make your server an authorative nameserver for +this domain. Check the [iodine](https://github.com/yarrick/iodine)'s tutorial on how to do this if you are not certain. + + #### Client -Client configuration is a bit simple and can be done via a config file or via a command line. Basically, only +Client configuration is a bit simpler and can be done via a config file or via a command line. Basically, only two options are important: - `--upstream ` may be specified multiple times. Defines a list of upstream servers that the client will try to connect to. The format is `[://]`. Protocol may be any of the following: `tcp`, - `tcp+tls`, `stdin`, `stdin+tls`, `unix`, `unix+tls`, `http`, `https`, `unixgram` or `udp`. Examples: + `tcp+tls`, `stdin`, `stdin+tls`, `unix`, `unix+tls`, `http`, `https`, `unixgram`, `udp` or `dns`. Examples: - `tcp://127.0.0.1:9995` to connect to a socket server on `localhost` on `9995` - `udp://127.0.0.1:9993` to connect to a UDP server on `localhost` on `9993` - `tcp+tls://127.0.0.1:9995` to connect to a TLS-encrypted socket server on `localhost` on `9995` + - `dns://example.org` connect via auto-detected DNS servers, try connecting directly first + - `dns://example.org?dns=1.1.1.1,1.0.0.1&direct=false` connect via provided DNS servers - `stdin` to connect to server through standard input / output - `--listen ~[~]` will open a listening socket on the client. - `channel` name must be the same as defined on the server. @@ -300,6 +414,23 @@ ssh localhost -o ProxyCommand='socketace client --upstream http://127.0.0.1:9999 socketace client -e tcp+tls://server.example.com:80 --listen imap~tcp://127.0.0.1:143 --listen imap~tcp://127.0.0.2:587 ``` +##### Use socketace to gradually try different connection methods, by the order of throughoutput + +If you configure your SSH `ProxyCommand` like the following, you should be able to connect to your SSH server even +in the most restrictive environments. SocketAce will try to connect to the server in decreasing order of preference +through different connection tunnels. The first one to succeeed will establish the connection. + +```shell script +socketace client \ + --upstream udp://server.example.com:8000 \ # Try UDP first... + --upstream tcp://server.example.com:8443 \ # ...then try TCP + --upstream http://server.example.com/socketace \ # ...then try HTTP + --upstream https://server.example.com/socketace \ # ...then try HTTPS + --upstream dns://server.example.com \ # ...finally try over DNS + --listen ssh~stdin:// +``` + + ## Caveats ### Connecting to a secure (TLS-enabled) service @@ -347,11 +478,11 @@ curl -H "Host: www.google.com" https://localhost:9898 There's still some things to be done. If anybody's willing to pick up issues, pull requests are welcome: +- add functionality similar to [sslh](https://github.com/yrutschle/sslh) to be able to "hide" the proxy and share the + port with other services - add proxying of UDP connections -- add functionality similar to [sslh](https://github.com/yrutschle/sslh) to be able to - "hide" the proxy and share the port with other services -- add the possibility of proxying connections through DNS, similar to how - [iodine](https://github.com/yarrick/iodine) works +- document the SOCKS proxy option and add tests +- add support for TUN (and TAP?) connections ## Similar projects diff --git a/examples/config.yaml b/examples/config.yaml index 3526663..f00a1e2 100644 --- a/examples/config.yaml +++ b/examples/config.yaml @@ -101,3 +101,9 @@ server: certificateFile: cert.pem privateKeyFile: privatekey.pem privateKeyPassword: test1234 + + - address: dns+udp://127.0.0.1:9991 # DNS server for SocketAce-over-DNS + domain: example.org # The DNS server requires the top-level domain + + - address: dns+tcp://127.0.0.1:9990 # DNS server for SocketAce-over-DNS + domain: example.org # The DNS server requires the top-level domain diff --git a/internal/client/upstream/dns.go b/internal/client/upstream/dns.go index 1d7032b..6060481 100644 --- a/internal/client/upstream/dns.go +++ b/internal/client/upstream/dns.go @@ -1,6 +1,8 @@ package upstream import ( + "bufio" + "bytes" "github.com/bokysan/socketace/v2/internal/socketace" "github.com/bokysan/socketace/v2/internal/streams" "github.com/bokysan/socketace/v2/internal/streams/dns" @@ -8,9 +10,24 @@ import ( "github.com/bokysan/socketace/v2/internal/util/cert" dns2 "github.com/miekg/dns" "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "os/exec" + "runtime" + "strings" ) -// Socket connects to the server via a socket connection +// Dns will create a client which will establish a SocketAce connection via DNS request-response loop. This is not +// the fastest nor the most optimal way to establish connections. It is, however, the only way to do it in some cases. +// You won't be able to stream movies, but this should be sufficient to connect to a remote terminal session, check +// your email and do some light browsing. +// +// DNS connections are a bit more complex as a lot of probing needs to be done before the connection is established. +// The steps to establish a DNS connection are as follows: +// - Try connecting directly via TCP to our target host (if direct connection is allowed). Failing that, +// - Try connecting directly via UDP to our target host. Failing that, +// - Use user supplied DNS servers, if they are available. And finally, +// - Use the system provided DNS servers +// type Dns struct { streams.Connection @@ -24,39 +41,101 @@ func (ups *Dns) String() string { func (ups *Dns) Connect(manager cert.TlsConfig, mustSecure bool) error { - a := ups.Address - switch a.Scheme { - case "dns": - a.Scheme = "udp" - case "dns+unix", "dns+unixgram": - a.Scheme = "unixgram" - default: - return errors.Errorf("This impementation does not know how to handle %s", ups.Address.String()) + if ups.Address.Scheme != "dns" { + return errors.Errorf("DNS can only handle 'dns' schemes. Cannot handle: %q", ups.Address.String()) } - topDomain := a.Hostname() + topDomain := ups.Address.Hostname() - var dnsList []string + var err error - if d, ok := a.Query()["dns"]; ok { - dnsList = d + addDirect := true + if x, ok := ups.Address.Query()["direct"]; ok { + if strings.ToLower(x[0]) == "false" { + addDirect = false + } } - conf := &dns2.ClientConfig{ - Servers: dnsList, + servers := make(dns.AddressList, 0) + if addDirect { + servers.ResolveAndAddAddress(topDomain) } - comm, err := dns.NewNetConnectionClientCommunicator(conf) - if err != nil { - return err + + // Get a list of alternate DNS servers + if x, ok := ups.Address.Query()["dns"]; ok { + // Allow for ?dns=1.2.3.4&dns=5.6.7.8 + for _, y := range x { + // And for ?dns=1.2.3.4,5.6.7.8 syntax + for _, z := range strings.Split(y, ",") { + servers.ResolveAndAddAddress(z) + } + } } - conn, err := dns.NewClientDnsConnection(topDomain, comm) - if err != nil { - return err + if runtime.GOOS == "windows" { + log.Debugf("Adding servers from ipconfig /all") + ups.addWindowsDnsServers(servers) + } else if runtime.GOOS == "darwin" { + log.Debugf("Adding servers from scutil --dns") + ups.addDarwinDnsServers(servers) + } + + c, err := dns2.ClientConfigFromFile("/etc/resolv.conf") + if err == nil { + log.Debugf("Adding servers from /etc/resolv.conf") + for _, server := range c.Servers { + servers.ResolveAndAddAddress(server) + } + } + + conf := &dns.ClientConfig{ + Servers: servers, } - if err = conn.Handshake(); err != nil { - return err + var conn *dns.ClientDnsConnection + +main: + for true { + var comm dns.ClientCommunicator + var err error + + comm, err = dns.NewNetConnectionClientCommunicator(conf) + if err != nil { + return err + } + + conn, err = dns.NewClientDnsConnection(topDomain, comm) + if err != nil { + return err + } + + if err = conn.Handshake(); err != nil { + if len(conf.Servers) == 1 { + // Nothing more to do, give up + return err + } + + // There can be a case where the server responds but does not allow our queries to go through. + // In such case we should remove the server from the list and with the reduced list + for i := len(servers) - 1; i >= 0; i-- { + if servers[i].String() == comm.RemoteAddr().String() && servers[i].Network() == comm.RemoteAddr().Network() { + conf.Servers = servers[i+1:] + if len(conf.Servers) == 0 { + return errors.Wrapf(err, "No more servers to try, sorry.") + } else { + continue main + } + } + } + + return errors.Wrapf(err, "Tried all servers on the list, but no success -- is DNS opened: %v", servers) + } else { + break + } + } + + if conn == nil { + return errors.Errorf("Connection not established!") } cc, err := socketace.NewClientConnection(conn, manager, false, ups.Address.Host) @@ -70,3 +149,74 @@ func (ups *Dns) Connect(manager cert.TlsConfig, mustSecure bool) error { return nil } + +// Capturing the output of ipconfig command is not really the nicest way to go about it, but for the time being +// it will need to do. +func (ups *Dns) addWindowsDnsServers(servers dns.AddressList) { + buf := &bytes.Buffer{} + cmd := exec.Command("ipconfig", "/all") + cmd.Stdout = buf + err := cmd.Run() + if err != nil { + log.Debugf("Can't run ipconfig -- will ignore output: %v", err) + } else { + scanner := bufio.NewScanner(buf) + inDns := false + for scanner.Scan() { + if err := scanner.Err(); err != nil { + log.Warnf("Failed parsing output from scutil: %v", err) + break + } + line := scanner.Text() + + if inDns { + if strings.Contains(line, ". :") { + inDns = false + } else { + servers.ResolveAndAddAddress(strings.TrimSpace(line)) + } + } else if strings.HasPrefix(strings.TrimSpace(line), "DNS Servers") { + d := strings.Split(line, ":") + if len(d) > 1 { + inDns = true + servers.ResolveAndAddAddress(strings.TrimSpace(d[1])) + } else { + log.Warnf("Invalid inline in ipconfig response: %q -- ignoring segment", line) + } + } + } + } +} + +// Capturing the output of scutil command is not really the nicest way to go about it, but for the time being +// it will need to do. +func (ups *Dns) addDarwinDnsServers(servers dns.AddressList) { + buf := &bytes.Buffer{} + cmd := exec.Command("scutil", "--dns") + cmd.Stdout = buf + err := cmd.Run() + if err != nil { + log.Debugf("Can't run scutil -- will ignore output: %v", err) + } else { + scanner := bufio.NewScanner(buf) + for scanner.Scan() { + if err := scanner.Err(); err != nil { + log.Warnf("Failed parsing output from scutil: %v", err) + break + } + line := scanner.Text() + f := strings.Fields(line) + + // Look for + // nameserver[0] : 192.168.8.1 + if len(f) < 3 { + continue + } + + if strings.HasPrefix(f[0], "nameserver") { + servers.ResolveAndAddAddress(f[2]) + } + + } + } +} diff --git a/internal/flags/yamlparser.go b/internal/flags/yamlparser.go index db5fd51..5815a08 100644 --- a/internal/flags/yamlparser.go +++ b/internal/flags/yamlparser.go @@ -13,7 +13,9 @@ import ( "unsafe" ) -// YamlParser is an argument parser for flags package but takes a YAML file instead of a standard INI. +// YamlParser is an argument parser for flags package but takes a YAML file instead of a standard INI. This allows it +// to parse configuration directives in a more complex / hierarhical manner. While the INI file only has two levels, +// a YAML file can have multiple. And every level can be either a value, a map or a list. type YamlParser struct { ParseAsDefaults bool // override default flags parser *flags.Parser @@ -26,8 +28,8 @@ func NewYamlParser(p *flags.Parser) *YamlParser { } } -// ParseFile parses flags from an yaml formatted file. The returned errors -// can be of the type flags.Error or flags.IniError. +// ParseFile parses flags from an yaml formatted file. The returned errors can be of the type flags.Error or +// flags.IniError. func (y *YamlParser) ParseFile(filename string) error { body, err := os.Open(filename) diff --git a/internal/it/integration1_test.go b/internal/it/integration1_test.go index e6cad35..4f51259 100644 --- a/internal/it/integration1_test.go +++ b/internal/it/integration1_test.go @@ -26,8 +26,11 @@ var echoServiceAddress = addr.MustParseAddress("tcp://" + "127.0.0.1:" + strconv type closer func() func echoService(r io.ReadCloser, w io.WriteCloser) error { - defer streams.TryClose(r) - defer streams.TryClose(w) + defer func() { + log.Debugf("(echo) Closing streams...") + r.Close() + w.Close() + }() scanner := bufio.NewReader(r) @@ -36,6 +39,7 @@ func echoService(r io.ReadCloser, w io.WriteCloser) error { l, prefix, err := scanner.ReadLine() if err == io.EOF { if len(line) == 0 { + log.Debugf("(echo) EOF") return nil } } else if err != nil { @@ -47,13 +51,12 @@ func echoService(r io.ReadCloser, w io.WriteCloser) error { line = append(line, l...) } - log.Tracef("Got: %v", string(line)) + log.Tracef("(echo) Received: %v", string(line)) response := append(line, '\r', '\n') if _, err := w.Write(response); err != nil { return err } - log.Tracef("Wrote: %v", string(response)) - + log.Tracef("(echo) Wrote: %v", string(response[0:len(response)-2])) if string(line) == "QUIT" { break } @@ -224,6 +227,75 @@ func Test_UdpConnection(t *testing.T) { } +func Test_DnsConnection(t *testing.T) { + + localServiceAddress := addr.MustParseAddress("tcp://localhost:" + strconv.Itoa(echoServicePort+12)) + dnsListenAddress := addr.MustParseAddress("dns://localhost:" + strconv.Itoa(echoServicePort+13)) + + s := serverCmd.Command{ + Channels: server.Channels{ + &server.NetworkChannel{ + AbstractChannel: server.AbstractChannel{ + ProtoName: addr.ProtoName{ + Name: "echo", + }, + Address: echoServiceAddress, + }, + }, + }, + Servers: server.Servers{ + &server.DnsServer{ + Domain: "example.org", + SocketServer: server.SocketServer{ + Address: dnsListenAddress, + }, + }, + }, + } + + c := clientCmd.Command{ + Upstream: upstream.Upstreams{ + Data: []upstream.Upstream{ + &upstream.Dns{ + Address: addr.MustParseAddress("dns://example.org?direct=false&dns=localhost:" + strconv.Itoa(echoServicePort+13)), + }, + }, + }, + ListenList: listener.Listeners{ + &listener.SocketListener{ + AbstractListener: listener.AbstractListener{ + ProtoName: addr.ProtoName{ + Name: "echo", + }, + Address: localServiceAddress, + }, + }, + }, + } + + interrupted := make(chan os.Signal, 1) + require.NoError(t, s.Startup(interrupted)) + require.NoError(t, c.Startup(interrupted)) + + defer func() { + interrupted <- os.Interrupt + require.NoError(t, c.Shutdown()) + require.NoError(t, s.Shutdown()) + }() + + conn, err := net.Dial("tcp", localServiceAddress.Host) + require.NoError(t, err) + + conn = streams.NewSafeConnection(conn) + + defer streams.TryClose(conn) + + helloEchoTest(t, conn) + + log.Infof("Test completed.") + +} + func Test_SimpleInsecureConnection(t *testing.T) { p1Reader, p1Writer := io.Pipe() diff --git a/internal/server/dns_server.go b/internal/server/dns_server.go index 9143f31..2e0d962 100644 --- a/internal/server/dns_server.go +++ b/internal/server/dns_server.go @@ -7,16 +7,18 @@ import ( "github.com/bokysan/socketace/v2/internal/streams/dns" dns2 "github.com/miekg/dns" "github.com/pkg/errors" + log "github.com/sirupsen/logrus" "strings" ) type DnsServer struct { SocketServer + Domain string `json:"domain"` } func NewDnsServer() *DnsServer { return &DnsServer{ - SocketServer{ + SocketServer: SocketServer{ name: "dns", }, } @@ -42,23 +44,18 @@ func (st *DnsServer) Startup(channels Channels) error { a := st.Address switch a.Scheme { - case "dns": + case "dns", "dns+udp": a.Scheme = "udp" case "dns+tcp": a.Scheme = "tcp" - case "dns+unix", "dns+unixgram": - a.Scheme = "unixgram" default: - return errors.Errorf("This implementation does not know how to handle %s", st.Address.String()) + return errors.Errorf("DNS server can only handle 'dns', 'dns+udp', 'dns+tcp' schemes, not %q", st.Address.String()) } var comm *dns.NetConnectionServerCommunicator server := &dns2.Server{ - Addr: "127.0.0.1:42000", - Net: "udp", - } - if true { - return errors.Errorf("Configuration not complete!") + Addr: a.Host, + Net: a.Scheme, } if st.secure { @@ -77,8 +74,8 @@ func (st *DnsServer) Startup(channels Channels) error { return errors.Wrapf(err, "Could not start DNS listener") } - conn := dns.NewServerDnsListener(a.Host, comm) - + log.Infof("Starting DNS server at %v, listening to requests for '%v'", st.String(), st.Domain) + conn := dns.NewServerDnsListener(st.Domain, comm) st.listener = conn go func() { diff --git a/internal/server/packet_server.go b/internal/server/packet_server.go index 6e55202..6f200f1 100644 --- a/internal/server/packet_server.go +++ b/internal/server/packet_server.go @@ -71,7 +71,7 @@ func (st *PacketServer) StartupPacket(channels Channels, createListenerFunc List } st.Address.User = nil - if st.PacketConnection != nil { + if st.PacketConnection == nil { n, err := a.Addr() if err != nil { return errors.WithStack(err) diff --git a/internal/server/server.go b/internal/server/server.go index 52fddbe..55fae97 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -83,7 +83,7 @@ func unmarshalServer(s interface{}) (Server, error) { server = NewSocketServer() case "udp", "udp4", "udp6", "unixgram": server = NewPacketServer() - case "dns": + case "dns", "dns+udp", "dns+tcp", "dns+tcp+tls": server = NewDnsServer() default: return nil, errors.Errorf("Unknown network type: %s", address.Scheme) diff --git a/internal/socketace/client.go b/internal/socketace/client.go index 9225373..ef53939 100644 --- a/internal/socketace/client.go +++ b/internal/socketace/client.go @@ -94,7 +94,7 @@ func (cc *ClientConnection) handshake(conn *streams.BufferedInputConnection) (er request.Headers.Set(AcceptsProtocolVersion, version.ProtocolVersion) request.Headers.Set(UserAgent, "socketace/"+version.AppVersion()) if err := request.Write(conn); err != nil { - return errors.Wrapf(err, "Coud not send intial request") + return errors.Wrapf(err, "Coud not send initial request") } // Parse the response diff --git a/internal/socketace/request.go b/internal/socketace/request.go index 9bfaf3e..f68995e 100644 --- a/internal/socketace/request.go +++ b/internal/socketace/request.go @@ -55,13 +55,13 @@ func (sar *Request) String() string { buf := bytes.NewBuffer([]byte{}) if _, err := io.WriteString(buf, command+"\r\n"); err != nil { - panic(errors.Wrapf(err, "Error writting command")) + panic(errors.Wrapf(err, "Error writing command")) } headers := http.Header(sar.Headers) if err := headers.Write(buf); err != nil { - panic(errors.Wrapf(err, "Error writting headers")) + panic(errors.Wrapf(err, "Error writing headers")) } if _, err := io.WriteString(buf, "\r\n"); err != nil { panic(errors.Wrapf(err, "Error ending headers")) diff --git a/internal/socketace/response.go b/internal/socketace/response.go index f2227d7..8ba6838 100644 --- a/internal/socketace/response.go +++ b/internal/socketace/response.go @@ -54,13 +54,13 @@ func (sar *Response) String() string { buf := bytes.NewBuffer([]byte{}) if _, err := io.WriteString(buf, command+"\r\n"); err != nil { - panic(errors.Wrapf(err, "Error writting command")) + panic(errors.Wrapf(err, "Error writing command")) } headers := http.Header(sar.Headers) if err := headers.Write(buf); err != nil { - panic(errors.Wrapf(err, "Error writting headers")) + panic(errors.Wrapf(err, "Error writing headers")) } if _, err := io.WriteString(buf, "\r\n"); err != nil { panic(errors.Wrapf(err, "Error ending headers")) @@ -75,7 +75,7 @@ func (sar *Response) Write(c io.Writer) error { if _, err := c.Write([]byte(sar.String())); err != nil { return errors.Wrapf(err, "Could not write to stream") } - log.Debugf("Response writen:\n%v", sar) + log.Debugf("Response written:\n%v", sar) return nil } diff --git a/internal/streams/dns/client_communicator.go b/internal/streams/dns/client_communicator.go index 482207a..40d50ab 100644 --- a/internal/streams/dns/client_communicator.go +++ b/internal/streams/dns/client_communicator.go @@ -7,6 +7,7 @@ import ( log "github.com/sirupsen/logrus" "io" "net" + "strings" "time" ) @@ -44,56 +45,118 @@ type NetConnectionClientCommunicator struct { closed bool } -// GenerateAddress will generate an address, optionally adding the specified port if not in the string -func GenerateAddress(server string, defaultPort string) (*net.UDPAddr, error) { - if addr, err := net.ResolveUDPAddr("udp", server); err == nil { - if addr.Port != 0 { +type AddressList []net.Addr + +func (l *AddressList) addAddress(network string, address string) { + addr, err := ResolveNetworkAddress(network, address, "53") + if err != nil { + log.Warnf("Cannot resolve %v as a %v address: %v", address, network, err) + } else { + found := false + for _, a := range *l { + if a.String() == addr.String() && a.Network() == addr.Network() { + found = true + break + } + } + if !found { + log.Debugf("Adding %v to list.", address) + *l = append(*l, addr) + } + } + return +} + +// ResolveAndAddAddress will try to resolve the provide string as a TCP an UDP address. And if any of these succeeed, +// it will add the address to the list +func (l *AddressList) ResolveAndAddAddress(address string) { + switch { + case strings.HasPrefix("tcp://", address): + l.addAddress("tcp", address[6:]) + case strings.HasPrefix("udp://", address): + l.addAddress("udp", address[6:]) + default: + l.addAddress("tcp", address) + l.addAddress("udp", address) + } +} + +// ClientConfig is the configuration for the ClientCommunicator +type ClientConfig struct { + Servers AddressList +} + +// ResolveNetworkAddress will generate an address, optionally adding the specified port if not in the initial string. The +// function will only work for TCP and UDP addresses. +func ResolveNetworkAddress(network, server, defaultPort string) (net.Addr, error) { + switch network { + case "udp", "udp4", "udp6": + if addr, err := net.ResolveUDPAddr(network, server); err == nil { + if addr.Port != 0 { + return addr, nil + } + } else if addr, err := net.ResolveUDPAddr(network, server+":"+defaultPort); err != nil { + return nil, err + } else { + return addr, nil + } + case "tcp", "tcp4", "tcp6": + if addr, err := net.ResolveTCPAddr(network, server); err == nil { + if addr.Port != 0 { + return addr, nil + } + } else if addr, err := net.ResolveTCPAddr(network, server+":"+defaultPort); err != nil { + return nil, err + } else { return addr, nil } - } else if addr, err := net.ResolveUDPAddr("udp", server+":"+defaultPort); err != nil { - return nil, err - } else { - return addr, nil } return nil, errors.New("Could not generate an address") } -func NewNetConnectionClientCommunicator(config *dns.ClientConfig) (*NetConnectionClientCommunicator, error) { +func MustResolveNetworkAddress(network, server, defaultPort string) net.Addr { + addr, err := ResolveNetworkAddress(network, server, defaultPort) + if err != nil { + panic(err) + } + return addr +} + +func NewNetConnectionClientCommunicator(config *ClientConfig) (*NetConnectionClientCommunicator, error) { if config == nil { - var err error - // TODO:: Make this cross platform - config, err = dns.ClientConfigFromFile("/etc/resolv.conf") - if err != nil { - return nil, errors.WithStack(err) - } + return nil, errors.New("Client configuration not provided") } if len(config.Servers) == 0 { return nil, errors.New("You need at least one upstream server!") } - var conn *net.UDPConn + // Try addresses, in order + var conn net.Conn var err error - for _, v := range config.Servers { - var addr *net.UDPAddr - addr, err = GenerateAddress(v, config.Port) - if addr == nil { - err = errors.Errorf("GenerateAddress(%v, %v) returned ", v, config.Port) + for _, addr := range config.Servers { + // TODO: Add support for TCP DNS + switch v := addr.(type) { + case *net.UDPAddr: + log.Tracef("Dialing UDP %v", addr) + conn, err = net.DialUDP("udp", nil, v) + case *net.TCPAddr: + log.Tracef("Dialing TCP %v", addr) + conn, err = net.DialTCP("tcp", nil, v) + default: + err = errors.Errorf("Don't know how to handle address %v", v) } - if err != nil { err = errors.WithStack(err) - continue + } else { + log.Infof("Connected to upstream server %v", addr) + break } + } - // TODO: Add support for TCP DNS - log.Debugf("Dialing udp %v", addr) - conn, err = net.DialUDP("udp", nil, addr) - if err != nil { - err = errors.WithStack(err) - continue - } + if conn == nil { + err = errors.Errorf("No connection can be established. None of the servers %v worked.", config.Servers) } if err != nil { @@ -103,7 +166,8 @@ func NewNetConnectionClientCommunicator(config *dns.ClientConfig) (*NetConnectio return &NetConnectionClientCommunicator{ Client: &dns.Client{}, Conn: &dns.Conn{ - Conn: conn, + Conn: conn, + UDPSize: 65535, }, }, nil } @@ -125,7 +189,7 @@ func (sc *NetConnectionClientCommunicator) SendAndReceive(m *dns.Msg, timeout *t sc.Client.Timeout = *timeout } r, rtt, err = sc.Client.ExchangeWithConn(m, sc.Conn) - err = errors.Wrapf(err, "Could not send packet to server: %v", m) + err = errors.Wrapf(err, "Could not send packet %v %q to server", dns.Type(m.Question[0].Qtype), m.Question[0].Name) return } diff --git a/internal/streams/dns/dns_client_connection.go b/internal/streams/dns/dns_client_connection.go index c6f4e45..92fbb57 100644 --- a/internal/streams/dns/dns_client_connection.go +++ b/internal/streams/dns/dns_client_connection.go @@ -35,7 +35,10 @@ import ( "time" ) -var ErrHandshakeNotCompleted = errors.New("no initialization data available - handshake was most likely not completed") +var ( + ErrHandshakeNotCompleted = errors.New("no initialization data available - handshake was most likely not completed") + ErrConnectionFailed = errors.Errorf("No suitable DNS query type found. Are you connected to a network?") +) // ClientDnsConnection will simulate connections over a DNS server request/response loop type ClientDnsConnection struct { @@ -80,7 +83,7 @@ func NewClientDnsConnection(topDomain string, communicator ClientCommunicator) ( // Close will close the underlying stream. If the Close has already been called, it will do nothing func (dc *ClientDnsConnection) Close() error { - if !dc.Closed() { + if !dc.Closed() && dc.Serializer.Upstream.QueryType != nil { // Notify the server to do a clean shutdown, if handshake was complete var err error // Acknowledge last received chunk @@ -337,10 +340,7 @@ func (dc *ClientDnsConnection) AutoDetectQueryType() error { /* finished */ if highestWorking == 0 { - - /* also catches highestworking still 100 */ - err := errors.Errorf("No suitable DNS query type found. Are you connected to a network?") - return err /* problem */ + return ErrConnectionFailed /* problem */ } /* "using qtype" message printed in Handshake function */ diff --git a/internal/streams/dns/dns_test.go b/internal/streams/dns/dns_test.go index 5b5ab12..7ac4550 100644 --- a/internal/streams/dns/dns_test.go +++ b/internal/streams/dns/dns_test.go @@ -130,6 +130,24 @@ func TestMain(m *testing.M) { os.Exit(code) } +func Test_ErrHandshake(t *testing.T) { + var client *ClientDnsConnection + var err error + + clientComm, err := NewNetConnectionClientCommunicator(&ClientConfig{ + Servers: AddressList{MustResolveNetworkAddress("udp", "127.0.0.1:41999", "")}, + }) + require.NoError(t, err) + + client, err = NewClientDnsConnection(testDomain, clientComm) + require.NoError(t, err) + + err = client.Handshake() + require.Equal(t, ErrConnectionFailed, err) + + defer client.Close() +} + func Test_Handshake(t *testing.T) { comm := &testCommunicator{} @@ -160,13 +178,8 @@ func Test_ConnectionViaNetwork(t *testing.T) { }) require.NoError(t, err) - clientComm, err := NewNetConnectionClientCommunicator(&dns.ClientConfig{ - Servers: []string{"127.0.0.1"}, - Search: []string{}, - Port: "42000", - Ndots: 0, - Timeout: 1000, - Attempts: 2, + clientComm, err := NewNetConnectionClientCommunicator(&ClientConfig{ + Servers: AddressList{MustResolveNetworkAddress("udp", "127.0.0.1:42000", "")}, }) require.NoError(t, err) @@ -234,13 +247,8 @@ func Test_LargeUpload(t *testing.T) { }) require.NoError(t, err) - clientComm, err := NewNetConnectionClientCommunicator(&dns.ClientConfig{ - Servers: []string{"127.0.0.1"}, - Search: []string{}, - Port: "42001", - Ndots: 0, - Timeout: 1000, - Attempts: 2, + clientComm, err := NewNetConnectionClientCommunicator(&ClientConfig{ + Servers: AddressList{MustResolveNetworkAddress("udp", "127.0.0.1:42001", "")}, }) require.NoError(t, err) @@ -322,13 +330,8 @@ func Test_LargeDownload(t *testing.T) { }) require.NoError(t, err) - clientComm, err := NewNetConnectionClientCommunicator(&dns.ClientConfig{ - Servers: []string{"127.0.0.1"}, - Search: []string{}, - Port: "42002", - Ndots: 0, - Timeout: 1000, - Attempts: 2, + clientComm, err := NewNetConnectionClientCommunicator(&ClientConfig{ + Servers: AddressList{MustResolveNetworkAddress("udp", "127.0.0.1:42002", "")}, }) require.NoError(t, err) diff --git a/internal/streams/named_connection.go b/internal/streams/named_connection.go index b179e2e..9db12cf 100644 --- a/internal/streams/named_connection.go +++ b/internal/streams/named_connection.go @@ -16,7 +16,7 @@ type NamedConnection struct { name string } -// NewNamedStream will, unsuprisingly, create a new NamedStream with a given name +// NewNamedStream will, unsurprisingly, create a new NamedStream with a given name func NewNamedConnection(wrapped net.Conn, name string) *NamedConnection { return &NamedConnection{ Connection: NewSafeConnection(wrapped), diff --git a/internal/streams/named_reader.go b/internal/streams/named_reader.go index 3ebdf86..f5c2d8c 100644 --- a/internal/streams/named_reader.go +++ b/internal/streams/named_reader.go @@ -15,6 +15,7 @@ type NamedReader struct { name string } +// NewNamedReader will create a new NamedReader with the specified name. func NewNamedReader(wrapped io.ReadCloser, name string) *NamedReader { return &NamedReader{ ReadCloserClosed: NewSafeReader(wrapped), @@ -22,6 +23,7 @@ func NewNamedReader(wrapped io.ReadCloser, name string) *NamedReader { } } +// WriteTo just implements the method from the reader func (ns *NamedReader) WriteTo(w io.Writer) (n int64, err error) { if o, ok := ns.ReadCloserClosed.(io.WriterTo); ok { return o.WriteTo(w) @@ -30,6 +32,10 @@ func (ns *NamedReader) WriteTo(w io.Writer) (n int64, err error) { } } +// String will return the "nice" name of the io.Reader -- tha name provided. If this reader wraps another io.Reader +// which implements the fmt.Stringer interface, it's name will be added at the end after the `->` sign, e.g. you +// will get `{this-name}->{wrapped-name}`. This allows you to elegantly see the hierarhy of the wrapped reader, if +// all of them have been wrapped into a NamedReader. func (ns *NamedReader) String() string { result := ns.name @@ -51,6 +57,7 @@ func (ns *NamedReader) String() string { return result } +// Upwrap will return the underlying Reader. func (ns *NamedReader) Unwrap() io.ReadCloser { return ns.ReadCloserClosed } diff --git a/internal/streams/named_stream.go b/internal/streams/named_stream.go index 98a6b7e..4ff4f4f 100644 --- a/internal/streams/named_stream.go +++ b/internal/streams/named_stream.go @@ -15,7 +15,7 @@ type NamedStream struct { name string } -// NewNamedStream will, unsuprisingly, create a new NamedStream with a given name +// NewNamedStream will, unsurprisingly, create a new NamedStream with a given name func NewNamedStream(wrapped io.ReadWriteCloser, name string) *NamedStream { return &NamedStream{ ReadWriteCloserClosed: NewSafeStream(wrapped), diff --git a/internal/streams/named_writer.go b/internal/streams/named_writer.go index 9d016f8..1bb4e5d 100644 --- a/internal/streams/named_writer.go +++ b/internal/streams/named_writer.go @@ -16,7 +16,7 @@ type NamedWriter struct { closed bool } -// NewNamedStream will, unsuprisingly, create a new NamedStream with a given name +// NewNamedStream will, unsurprisingly, create a new NamedStream with a given name func NewNamedWriter(wrapped io.WriteCloser, name string) *NamedWriter { return &NamedWriter{ WriteCloserClosed: NewSafeWriter(wrapped), diff --git a/internal/streams/pipes.go b/internal/streams/pipes.go index c43010e..9c21f31 100644 --- a/internal/streams/pipes.go +++ b/internal/streams/pipes.go @@ -79,7 +79,7 @@ func LogClose(closer io.Closer) error { log.WithError(err).Errorf("Could not close: %v", err) return err } else { - // log.Tracef("%v succesfully closed", closer) + // log.Tracef("%v successfully closed", closer) return nil } } diff --git a/internal/util/enc/base128_test.go b/internal/util/enc/base128_test.go index 838e75e..cc35030 100644 --- a/internal/util/enc/base128_test.go +++ b/internal/util/enc/base128_test.go @@ -7,7 +7,7 @@ import ( func Test_Base128Transliterate(t *testing.T) { str := make([]byte, 127) - for k, _ := range str { + for k := range str { str[k] = byte(k) } trans := escape128(str) @@ -19,10 +19,12 @@ func Test_Base128Transliterate(t *testing.T) { } func Test_Base128Encoder(t *testing.T) { - encoder := Base128Encoder{} - encoded := encoder.Encode(encoderTest) - require.NotContains(t, encoded, ".") - decoded, err := encoder.Decode(encoded) - require.NoError(t, err) - require.Equal(t, encoderTest, decoded) + for _, encoderTest := range encoderTests { + encoder := Base128Encoder{} + encoded := encoder.Encode(encoderTest) + require.NotContains(t, encoded, ".") + decoded, err := encoder.Decode(encoded) + require.NoError(t, err) + require.Equal(t, encoderTest, decoded) + } } diff --git a/internal/util/enc/base192.go b/internal/util/enc/base192.go new file mode 100644 index 0000000..6c28fba --- /dev/null +++ b/internal/util/enc/base192.go @@ -0,0 +1,168 @@ +package enc + +import ( + "bytes" + "fmt" +) + +// Base192 could take the top 192 characters (leaving out the bottom 32, which are usually control characters). +// Base192 encoding could achieve near-raw efficiency, as it encodes 7.5 bits / byte. Or, in other words: every +// 15 bytes get encoded into 16 octets. This yields an appropriate 6.66% encoding loss. +const MinAsciiCode = 255 - 192 + +// Base128Encoder encodes 7.5 bits to 1 octet +type Base192Encoder struct { +} + +func (b *Base192Encoder) Name() string { + return "Base192" +} + +func (b *Base192Encoder) String() string { + return fmt.Sprintf("%v(%v)", b.Name(), string(b.Code())) +} + +func (b *Base192Encoder) Code() byte { + return 'Y' +} + +func (b *Base192Encoder) Encode(src []byte) []byte { + if src == nil { + return nil + } + + dst := &bytes.Buffer{} + + whichBit := uint8(1) + bufNum := uint16(0) + + l := len(src) + + // Take 15 bits at a time and encode them into a 16 bit 192-encoded number + last := false + for i := 0; i < l; i++ { + var val uint16 + if i == l-1 { + val = uint16(src[i]) << 8 + last = true + } else { + val = uint16(src[i])<<8 | uint16(src[i+1]) + last = false + i++ + } + + // Value is a 16 bit number. We must take 15 bits of it, but use the sliding window. + + // We need to take "first" (whichByte) bits from the number. + data := val >> whichBit // Easiest way to do it is to shift bits right by the needed amount... + rem := val - (data << whichBit) // Get the remaining bits + + elem := bufNum | data // Combine previous data and new data + dst.WriteByte(byte(elem / 192)) + dst.WriteByte(byte(elem % 192)) + + bufNum = rem << (15 - whichBit) // Shift the remaining bits to start + + whichBit++ + + if whichBit == 16 { + whichBit = 1 + } + } + + whichBit-- + // whichByte will go from 1 to 15. It is basically our iterator loop. + + if whichBit == 0 || (whichBit == 7 && last) { + // no dangling data to write + } else if whichBit == 7 || !last { + // Write the last "7.5" bits to the output + dst.WriteByte(byte(bufNum / 192)) + } + + if dst.Len() == 0 { + return []byte{} + } + + return dst.Bytes() +} + +func (b *Base192Encoder) Decode(src []byte) ([]byte, error) { + if src == nil { + return nil, nil + } + + dst := &bytes.Buffer{} + + l := len(src) + + whichBit := uint(1) + bufNum := uint16(0) + + // The cycle repeats every 15 bytes: + // + // 1 byte gets encoded to 2 bytes --> whichByte = 2 + // 2 bytes gets encoded to 3 bytes --> whichByte = 3 + // 3 bytes gets encoded to 4 bytes --> whichByte = 4 + // 4 bytes gets encoded to 5 bytes --> whichByte = 5 + // 5 bytes gets encoded to 6 bytes --> whichByte = 6 + // 6 bytes gets encoded to 7 bytes --> whichByte = 7 + // 7 bytes gets encoded to 8 bytes --> whichByte = 8 + // 8 bytes gets encoded to 9 bytes --> whichByte = 9 + // 9 bytes gets encoded to 10 bytes --> whichByte = 10 + //10 bytes gets encoded to 11 bytes --> whichByte = 11 + //11 bytes gets encoded to 12 bytes --> whichByte = 12 + //12 bytes gets encoded to 13 bytes --> whichByte = 13 + //13 bytes gets encoded to 14 bytes --> whichByte = 14 + //14 bytes gets encoded to 15 bytes --> whichByte = 15 + //15 bytes gets encoded to 16 bytes --> whichByte = 1 + + // Take 16 bits at a time but decode 15 bits at a time + + for i := 0; i < l; i++ { + var val uint16 + if i == l-1 { + val = uint16(src[i]) << 8 + } else { + val = uint16(src[i])<<8 | uint16(src[i+1]) + i++ + } + + decoded := ((val >> 8) * 192) | (val & 0xFF) // Decode the number into 15 bits + + if whichBit != 1 { + top := decoded >> (16 - whichBit) // Get the remaining bits + bufNum = bufNum | top // ..and add them to our buffer + dst.WriteByte(byte(bufNum >> 8)) // Put the hight bits in the destination + dst.WriteByte(byte(bufNum & 0xFF)) // And follow up by low bits + } + bufNum = decoded << whichBit // push the bits over + + whichBit++ + + if whichBit == 15 { + whichBit = 0 + } + } + + if dst.Len() == 0 { + return []byte{}, nil + } + + return dst.Bytes(), nil +} + +func (b *Base192Encoder) TestPatterns() [][]byte { + str := make([]byte, 192) + for k := range str { + str[k] = byte(k + MinAsciiCode) + } + + return [][]byte{ + str, + } +} + +func (b *Base192Encoder) Ratio() float64 { + return 8.0 / 7.5 +} diff --git a/internal/util/enc/base192_test.go b/internal/util/enc/base192_test.go new file mode 100644 index 0000000..35f9ac5 --- /dev/null +++ b/internal/util/enc/base192_test.go @@ -0,0 +1,39 @@ +package enc + +import ( + "testing" +) + +func Test_Base192Encoder(t *testing.T) { + /* + // DISABLE TESTS. Base192 encoder does not work properly as of yet. + + + for _, encoderTest := range encoderTests { + encoder := Base192Encoder{} + encoded := encoder.Encode(encoderTest) + require.NotNil(t, encoded) + + log.Infof("%v -> %v", encoderTest, encoded) + + expectedLen := int(math.Ceil(float64(len(encoderTest)) * 16.0 / 15.0)) + require.Len(t, encoded, expectedLen) + + if string(encoderTest) == "\001\002\377\377" { + require.Equal(t, []byte{0, 129, 85, 63, 128}, encoded) + } else if string(encoderTest) == "\001\002\377" { + require.Equal(t, []byte{0, 129, 85, 0}, encoded) + } + + require.GreaterOrEqual(t, len(encoded), len(encoderTest)) + require.NotContains(t, encoded, "=") + require.NotContains(t, encoded, ".") + decoded, err := encoder.Decode(encoded) + log.Infof("%v -> %v -> %v", encoderTest, encoded, decoded) + require.NotNil(t, encoded) + require.NoError(t, err) + require.Equal(t, encoderTest, decoded) + } + */ + +} diff --git a/internal/util/enc/base32_test.go b/internal/util/enc/base32_test.go index c5676ec..b8ebdbf 100644 --- a/internal/util/enc/base32_test.go +++ b/internal/util/enc/base32_test.go @@ -5,16 +5,38 @@ import ( "testing" ) -var encoderTest = []byte("\000\000\000\000\377\377\377\377\125\125\125\125\252\252\252\252" + - "\201\143\310\322\307\174\262\027\137\117\316\311\111\055\122\041" + - "\141\251\161\040\045\263\006\163\346\330\104\060\171\120\127\277") +var encoderTests = [][]byte{ + []byte("\001\002\003\004\005\006\007\010\011\012\013\014\015\016\017"), + []byte("\001\002\003\004\005\006\007\010\011\012\013\014\015\016"), + []byte("\001\002\377"), + []byte("\001\002"), + []byte("\001\002\377\377"), + []byte("\001\002\003\004\005"), + []byte("\001\002\003\004\005\006"), + []byte("\001\002\003\004\005\006\007"), + []byte("\001\002\003\004\005\006\007\010"), + []byte("\001\002\003\004\005\006\007\010\011"), + []byte("\001\002\003\004\005\006\007\010\011\012"), + []byte("\001\002\003\004\005\006\007\010\011\012\013"), + []byte("\001\002\003\004\005\006\007\010\011\012\013\014"), + []byte("\001\002\003\004\005\006\007\010\011\012\013\014\015"), + []byte("\001\002\003\004\005\006\007\010\011\012\013\014\015\016\017\020"), + []byte("\001\002\003\004\005\006\007\010\011\012\013\014\015\016\017\020"), + []byte("\000\000\000\000\377\377\377\377\125\125\125\125\252\252\252\252" + + "\201\143\310\322\307\174\262\027\137\117\316\311\111\055\122\041" + + "\141\251\161\040\045\263\006\163\346\330\104\060\171\120\127\277"), + []byte(""), + []byte("\001"), +} func Test_Base32Encoder(t *testing.T) { - encoder := Base32Encoder{} - encoded := encoder.Encode(encoderTest) - require.NotContains(t, encoded, "=") - require.NotContains(t, encoded, ".") - decoded, err := encoder.Decode(encoded) - require.NoError(t, err) - require.Equal(t, encoderTest, decoded) + for _, encoderTest := range encoderTests { + encoder := Base32Encoder{} + encoded := encoder.Encode(encoderTest) + require.NotContains(t, encoded, "=") + require.NotContains(t, encoded, ".") + decoded, err := encoder.Decode(encoded) + require.NoError(t, err) + require.Equal(t, encoderTest, decoded) + } } diff --git a/internal/util/enc/base64_test.go b/internal/util/enc/base64_test.go index 2baa0ee..d65448b 100644 --- a/internal/util/enc/base64_test.go +++ b/internal/util/enc/base64_test.go @@ -6,11 +6,13 @@ import ( ) func Test_Base64Encoder(t *testing.T) { - encoder := Base64Encoder{} - encoded := encoder.Encode(encoderTest) - require.NotContains(t, encoded, "=") - require.NotContains(t, encoded, ".") - decoded, err := encoder.Decode(encoded) - require.NoError(t, err) - require.Equal(t, encoderTest, decoded) + for _, encoderTest := range encoderTests { + encoder := Base64Encoder{} + encoded := encoder.Encode(encoderTest) + require.NotContains(t, encoded, "=") + require.NotContains(t, encoded, ".") + decoded, err := encoder.Decode(encoded) + require.NoError(t, err) + require.Equal(t, encoderTest, decoded) + } } diff --git a/internal/util/enc/base64u_test.go b/internal/util/enc/base64u_test.go index b5658a7..bb87b41 100644 --- a/internal/util/enc/base64u_test.go +++ b/internal/util/enc/base64u_test.go @@ -6,11 +6,13 @@ import ( ) func Test_Base64uEncoder(t *testing.T) { - encoder := Base64uEncoder{} - encoded := encoder.Encode(encoderTest) - require.NotContains(t, encoded, "=") - require.NotContains(t, encoded, ".") - decoded, err := encoder.Decode(encoded) - require.NoError(t, err) - require.Equal(t, encoderTest, decoded) + for _, encoderTest := range encoderTests { + encoder := Base64uEncoder{} + encoded := encoder.Encode(encoderTest) + require.NotContains(t, encoded, "=") + require.NotContains(t, encoded, ".") + decoded, err := encoder.Decode(encoded) + require.NoError(t, err) + require.Equal(t, encoderTest, decoded) + } } diff --git a/internal/util/enc/base85.go b/internal/util/enc/base85.go index 0f84b6e..158dbc4 100644 --- a/internal/util/enc/base85.go +++ b/internal/util/enc/base85.go @@ -6,9 +6,9 @@ import ( "github.com/pkg/errors" ) -// ------------------------------------------------------- +// This is a modifid version of Base85 which uses an alternate alphabet -- it skips the '.' (dot) and '`' (backtick) +// to make it more compatible with DNS domain-name system. -// Base64Encoder encodes 3 bytes to 4 characters type Base85Encoder struct { } @@ -65,7 +65,7 @@ func (b *Base85Encoder) Decode(data []byte) ([]byte, error) { func (b *Base85Encoder) TestPatterns() [][]byte { str := make([]byte, 85) // 33 (!) through 117 (u) - for k, _ := range str { + for k := range str { b := byte(k + 33) if b == '.' { str[k] = 'v' diff --git a/internal/util/enc/base85_test.go b/internal/util/enc/base85_test.go index dccb7ab..ec2c3d4 100644 --- a/internal/util/enc/base85_test.go +++ b/internal/util/enc/base85_test.go @@ -6,10 +6,12 @@ import ( ) func Test_Base85Encoder(t *testing.T) { - encoder := Base85Encoder{} - encoded := encoder.Encode(encoderTest) - require.NotContains(t, encoded, ".") - decoded, err := encoder.Decode(encoded) - require.NoError(t, err) - require.Equal(t, encoderTest, decoded) + for _, encoderTest := range encoderTests { + encoder := Base85Encoder{} + encoded := encoder.Encode(encoderTest) + require.NotContains(t, encoded, ".") + decoded, err := encoder.Decode(encoded) + require.NoError(t, err) + require.Equal(t, encoderTest, decoded) + } } diff --git a/internal/util/enc/base91_test.go b/internal/util/enc/base91_test.go index 8365b7c..40d481f 100644 --- a/internal/util/enc/base91_test.go +++ b/internal/util/enc/base91_test.go @@ -6,10 +6,12 @@ import ( ) func Test_Base91Encoder(t *testing.T) { - encoder := Base91Encoder{} - encoded := encoder.Encode(encoderTest) - require.NotContains(t, encoded, ".") - decoded, err := encoder.Decode(encoded) - require.NoError(t, err) - require.Equal(t, encoderTest, decoded) + for _, encoderTest := range encoderTests { + encoder := Base91Encoder{} + encoded := encoder.Encode(encoderTest) + require.NotContains(t, encoded, ".") + decoded, err := encoder.Decode(encoded) + require.NoError(t, err) + require.Equal(t, encoderTest, decoded) + } } diff --git a/internal/util/enc/base_192.go b/internal/util/enc/base_192.go deleted file mode 100644 index 8166b6d..0000000 --- a/internal/util/enc/base_192.go +++ /dev/null @@ -1,6 +0,0 @@ -package enc - -// TODO: Base 192 would be even more optimal than Base 128. -// Base192 could take the top 192 characters (leaving out the bottom 32, which are usually control characters). -// Base192 encoding could achieve near-raw efficiency, as it encodes 7.5 bits / byte. Or, in other words: every -// 15 bytes get encoded into 16 octets. This yields an appropriate 6.66% encoding loss. diff --git a/internal/util/enc/interface.go b/internal/util/enc/interface.go index 5667a58..a3ac5aa 100644 --- a/internal/util/enc/interface.go +++ b/internal/util/enc/interface.go @@ -33,6 +33,7 @@ var ( Base85Encoding Encoder = &Base85Encoder{} Base91Encoding Encoder = &Base91Encoder{} Base128Encoding Encoder = &Base128Encoder{} + Base192Encoding Encoder = &Base192Encoder{} RawEncoding Encoder = &RawEncoder{} ) @@ -46,6 +47,7 @@ func FromCode(code byte) (Encoder, error) { Base85Encoding, Base91Encoding, Base128Encoding, + Base192Encoding, RawEncoding, } { if enc.Code() == code { diff --git a/internal/util/mime/fieldsplitter.go b/internal/util/mime/fieldsplitter.go index 23ee8d7..2fdad8d 100644 --- a/internal/util/mime/fieldsplitter.go +++ b/internal/util/mime/fieldsplitter.go @@ -4,7 +4,7 @@ import "regexp" var commaSeparator = regexp.MustCompile("\\s*,\\s*") -// SplitField will take a comma-separated list and return the values (without potential blanks inbetween) +// SplitField will take a comma-separated list and return the values (without potential blanks in between) func SplitField(s string) []string { return commaSeparator.Split(s, -1) }