<commit_before>package cmd

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"testing"

	"github.com/exercism/cli/config"
	"github.com/spf13/pflag"
	"github.com/spf13/viper"
	"github.com/stretchr/testify/assert"
)

func TestDownloadWithoutToken(t *testing.T) {
	cfg := config.Configuration{
		UserViperConfig: viper.New(),
	}

	err := runDownload(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{})
	if assert.Error(t, err) {
		assert.Regexp(t, "Welcome to Exercism", err.Error())
		// It uses the default base API url to infer the host
		assert.Regexp(t, "exercism.io/my/settings", err.Error())
	}
}

func TestDownloadWithoutWorkspace(t *testing.T) {
	v := viper.New()
	v.Set("token", "abc123")
	cfg := config.Configuration{
		UserViperConfig: v,
	}

	err := runDownload(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{})
	if assert.Error(t, err) {
		assert.Regexp(t, "re-run the configure", err.Error())
	}
}

func TestDownloadWithoutBaseURL(t *testing.T) {
	v := viper.New()
	v.Set("token", "abc123")
	v.Set("workspace", "/home/whatever")
	cfg := config.Configuration{
		UserViperConfig: v,
	}

	err := runDownload(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{})
	if assert.Error(t, err) {
		assert.Regexp(t, "re-run the configure", err.Error())
	}
}

func TestDownloadWithoutFlags(t *testing.T) {
	v := viper.New()
	v.Set("token", "abc123")
	v.Set("workspace", "/home/username")
	v.Set("apibaseurl", "http://example.com")

	cfg := config.Configuration{
		UserViperConfig: v,
	}

	flags := pflag.NewFlagSet("fake", pflag.PanicOnError)
	setupDownloadFlags(flags)

	err := runDownload(cfg, flags, []string{})
	if assert.Error(t, err) {
		assert.Regexp(t, "need an --exercise name or a solution --uuid", err.Error())
	}
}

func TestDownload(t *testing.T) {
	oldOut := Out
	oldErr := Err
	Out = ioutil.Discard
	Err = ioutil.Discard
	defer func() {
		Out = oldOut
		Err = oldErr
	}()

	testCases := []struct {
		requestor   string
		expectedDir string
		flags       map[string]string
	}{
		{
			requestor:   requestorSelf,
			expectedDir: "",
			flags:       map[string]string{"exercise": "bogus-exercise"},
		},
		{
			requestor:   requestorSelf,
			expectedDir: "",
			flags:       map[string]string{"uuid": "bogus-id"},
		},
		{
			requestor:   requestorOther,
			expectedDir: filepath.Join("users", "alice"),
			flags:       map[string]string{"uuid": "bogus-id"},
		},
	}

	for _, tc := range testCases {
		tmpDir, err := ioutil.TempDir("", "download-cmd")
		defer os.RemoveAll(tmpDir)
		assert.NoError(t, err)

		ts := fakeDownloadServer(tc.requestor)
		defer ts.Close()

		v := viper.New()
		v.Set("workspace", tmpDir)
		v.Set("apibaseurl", ts.URL)
		v.Set("token", "abc123")

		cfg := config.Configuration{
			UserViperConfig: v,
		}
		flags := pflag.NewFlagSet("fake", pflag.PanicOnError)
		setupDownloadFlags(flags)
		for name, value := range tc.flags {
			flags.Set(name, value)
		}

		err = runDownload(cfg, flags, []string{})
		assert.NoError(t, err)

		targetDir := filepath.Join(tmpDir, tc.expectedDir)
		assertDownloadedCorrectFiles(t, targetDir, tc.requestor)

		metadata := `{
			"track": "bogus-track",
			"exercise":"bogus-exercise",
			"id":"bogus-id",
			"url":"",
			"handle":"alice",
			"is_requester":%s,
			"auto_approve":false
		}`
		metadata = fmt.Sprintf(metadata, tc.requestor)
		metadata = compact(t, metadata)

		path := filepath.Join(targetDir, "bogus-track", "bogus-exercise", ".solution.json")
		b, err := ioutil.ReadFile(path)
		assert.NoError(t, err)
		assert.Equal(t, metadata, string(b), "the solution metadata file")
	}
}

func fakeDownloadServer(requestor string) *httptest.Server {
	mux := http.NewServeMux()
	server := httptest.NewServer(mux)

	path1 := "file-1.txt"
	mux.HandleFunc("/"+path1, func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "this is file 1")
	})

	path2 := "subdir/file-2.txt"
	mux.HandleFunc("/"+path2, func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "this is file 2")
	})

	path3 := "file-3.txt"
	mux.HandleFunc("/"+path3, func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "")
	})

	payloadBody := fmt.Sprintf(payloadTemplate, requestor, server.URL+"/", path1, path2, path3)
	mux.HandleFunc("/solutions/latest", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, payloadBody)
	})
	mux.HandleFunc("/solutions/bogus-id", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, payloadBody)
	})

	return server
}

func assertDownloadedCorrectFiles(t *testing.T, targetDir, requestor string) {
	expectedFiles := []struct {
		desc     string
		path     string
		contents string
	}{
		{
			desc:     "a file in the exercise root directory",
			path:     filepath.Join(targetDir, "bogus-track", "bogus-exercise", "file-1.txt"),
			contents: "this is file 1",
		},
		{
			desc:     "a file in a subdirectory",
			path:     filepath.Join(targetDir, "bogus-track", "bogus-exercise", "subdir", "file-2.txt"),
			contents: "this is file 2",
		},
	}

	for _, file := range expectedFiles {
		t.Run(file.desc, func(t *testing.T) {
			b, err := ioutil.ReadFile(file.path)
			assert.NoError(t, err)
			assert.Equal(t, file.contents, string(b))
		})
	}

	path := filepath.Join(targetDir, "bogus-track", "bogus-exercise", "file-3.txt")
	_, err := os.Lstat(path)
	assert.True(t, os.IsNotExist(err), "It should not write the file if empty.")
}

const requestorSelf = "true"
const requestorOther = "false"

const payloadTemplate = `
{
	"solution": {
		"id": "bogus-id",
		"user": {
			"handle": "alice",
			"is_requester": %s
		},
		"exercise": {
			"id": "bogus-exercise",
			"instructions_url": "http://example.com/bogus-exercise",
			"auto_approve": false,
			"track": {
				"id": "bogus-track",
				"language": "Bogus Language"
			}
		},
		"file_download_base_url": "%s",
		"files": [
		"%s",
		"%s",
		"%s"
		],
		"iteration": {
			"submitted_at": "2017-08-21t10:11:12.130z"
		}
	}
}
`

func compact(t *testing.T, s string) string {
	buffer := new(bytes.Buffer)
	err := json.Compact(buffer, []byte(s))
	assert.NoError(t, err)
	return buffer.String()
}
<commit_msg>Don't pass files to payload template in download test<commit_after>package cmd

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"testing"

	"github.com/exercism/cli/config"
	"github.com/spf13/pflag"
	"github.com/spf13/viper"
	"github.com/stretchr/testify/assert"
)

func TestDownloadWithoutToken(t *testing.T) {
	cfg := config.Configuration{
		UserViperConfig: viper.New(),
	}

	err := runDownload(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{})
	if assert.Error(t, err) {
		assert.Regexp(t, "Welcome to Exercism", err.Error())
		// It uses the default base API url to infer the host
		assert.Regexp(t, "exercism.io/my/settings", err.Error())
	}
}

func TestDownloadWithoutWorkspace(t *testing.T) {
	v := viper.New()
	v.Set("token", "abc123")
	cfg := config.Configuration{
		UserViperConfig: v,
	}

	err := runDownload(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{})
	if assert.Error(t, err) {
		assert.Regexp(t, "re-run the configure", err.Error())
	}
}

func TestDownloadWithoutBaseURL(t *testing.T) {
	v := viper.New()
	v.Set("token", "abc123")
	v.Set("workspace", "/home/whatever")
	cfg := config.Configuration{
		UserViperConfig: v,
	}

	err := runDownload(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{})
	if assert.Error(t, err) {
		assert.Regexp(t, "re-run the configure", err.Error())
	}
}

func TestDownloadWithoutFlags(t *testing.T) {
	v := viper.New()
	v.Set("token", "abc123")
	v.Set("workspace", "/home/username")
	v.Set("apibaseurl", "http://example.com")

	cfg := config.Configuration{
		UserViperConfig: v,
	}

	flags := pflag.NewFlagSet("fake", pflag.PanicOnError)
	setupDownloadFlags(flags)

	err := runDownload(cfg, flags, []string{})
	if assert.Error(t, err) {
		assert.Regexp(t, "need an --exercise name or a solution --uuid", err.Error())
	}
}

func TestDownload(t *testing.T) {
	oldOut := Out
	oldErr := Err
	Out = ioutil.Discard
	Err = ioutil.Discard
	defer func() {
		Out = oldOut
		Err = oldErr
	}()

	testCases := []struct {
		requestor   string
		expectedDir string
		flags       map[string]string
	}{
		{
			requestor:   requestorSelf,
			expectedDir: "",
			flags:       map[string]string{"exercise": "bogus-exercise"},
		},
		{
			requestor:   requestorSelf,
			expectedDir: "",
			flags:       map[string]string{"uuid": "bogus-id"},
		},
		{
			requestor:   requestorOther,
			expectedDir: filepath.Join("users", "alice"),
			flags:       map[string]string{"uuid": "bogus-id"},
		},
	}

	for _, tc := range testCases {
		tmpDir, err := ioutil.TempDir("", "download-cmd")
		defer os.RemoveAll(tmpDir)
		assert.NoError(t, err)

		ts := fakeDownloadServer(tc.requestor)
		defer ts.Close()

		v := viper.New()
		v.Set("workspace", tmpDir)
		v.Set("apibaseurl", ts.URL)
		v.Set("token", "abc123")

		cfg := config.Configuration{
			UserViperConfig: v,
		}
		flags := pflag.NewFlagSet("fake", pflag.PanicOnError)
		setupDownloadFlags(flags)
		for name, value := range tc.flags {
			flags.Set(name, value)
		}

		err = runDownload(cfg, flags, []string{})
		assert.NoError(t, err)

		targetDir := filepath.Join(tmpDir, tc.expectedDir)
		assertDownloadedCorrectFiles(t, targetDir, tc.requestor)

		metadata := `{
			"track": "bogus-track",
			"exercise":"bogus-exercise",
			"id":"bogus-id",
			"url":"",
			"handle":"alice",
			"is_requester":%s,
			"auto_approve":false
		}`
		metadata = fmt.Sprintf(metadata, tc.requestor)
		metadata = compact(t, metadata)

		path := filepath.Join(targetDir, "bogus-track", "bogus-exercise", ".solution.json")
		b, err := ioutil.ReadFile(path)
		assert.NoError(t, err)
		assert.Equal(t, metadata, string(b), "the solution metadata file")
	}
}

func fakeDownloadServer(requestor string) *httptest.Server {
	mux := http.NewServeMux()
	server := httptest.NewServer(mux)

	mux.HandleFunc("/file-1.txt", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "this is file 1")
	})

	mux.HandleFunc("/subdir/file-2.txt", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "this is file 2")
	})

	mux.HandleFunc("/file-3.txt", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "")
	})

	payloadBody := fmt.Sprintf(payloadTemplate, requestor, server.URL+"/")
	mux.HandleFunc("/solutions/latest", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, payloadBody)
	})
	mux.HandleFunc("/solutions/bogus-id", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, payloadBody)
	})

	return server
}

func assertDownloadedCorrectFiles(t *testing.T, targetDir, requestor string) {
	expectedFiles := []struct {
		desc     string
		path     string
		contents string
	}{
		{
			desc:     "a file in the exercise root directory",
			path:     filepath.Join(targetDir, "bogus-track", "bogus-exercise", "file-1.txt"),
			contents: "this is file 1",
		},
		{
			desc:     "a file in a subdirectory",
			path:     filepath.Join(targetDir, "bogus-track", "bogus-exercise", "subdir", "file-2.txt"),
			contents: "this is file 2",
		},
	}

	for _, file := range expectedFiles {
		t.Run(file.desc, func(t *testing.T) {
			b, err := ioutil.ReadFile(file.path)
			assert.NoError(t, err)
			assert.Equal(t, file.contents, string(b))
		})
	}

	path := filepath.Join(targetDir, "bogus-track", "bogus-exercise", "file-3.txt")
	_, err := os.Lstat(path)
	assert.True(t, os.IsNotExist(err), "It should not write the file if empty.")
}

const requestorSelf = "true"
const requestorOther = "false"

const payloadTemplate = `
{
	"solution": {
		"id": "bogus-id",
		"user": {
			"handle": "alice",
			"is_requester": %s
		},
		"exercise": {
			"id": "bogus-exercise",
			"instructions_url": "http://example.com/bogus-exercise",
			"auto_approve": false,
			"track": {
				"id": "bogus-track",
				"language": "Bogus Language"
			}
		},
		"file_download_base_url": "%s",
		"files": [
		"/file-1.txt",
		"/subdir/file-2.txt",
		"/file-3.txt"
		],
		"iteration": {
			"submitted_at": "2017-08-21t10:11:12.130z"
		}
	}
}
`

func compact(t *testing.T, s string) string {
	buffer := new(bytes.Buffer)
	err := json.Compact(buffer, []byte(s))
	assert.NoError(t, err)
	return buffer.String()
}
<|endoftext|>


<commit_before>/* Copyright (c) 2015, Daniel Martí <mvdan@mvdan.cc> */
/* See LICENSE for licensing information */

package main

import (
	"fmt"
	"log"
	"os"
	"strings"

	"github.com/mvdan/fdroidcl"
)

var cmdShow = &Command{
	UsageLine: "show <appid...>",
	Short:     "Show detailed info about an app",
}

func init() {
	cmdShow.Run = runShow
}

func findApps(ids []string) []*fdroidcl.App {
	index := mustLoadIndex()
	apps := make(map[string]*fdroidcl.App, len(index.Apps))
	for i := range index.Apps {
		app := &index.Apps[i]
		apps[app.ID] = app
	}
	result := make([]*fdroidcl.App, len(ids))
	for i, id := range ids {
		app, _ := apps[id]
		result[i] = app
	}
	return result
}

func runShow(args []string) {
	apps := findApps(args)
	for i, app := range apps {
		if app == nil {
			log.Fatalf("Could not find app with ID '%s'", args[i])
		}
		if i > 0 {
			fmt.Printf("\n--\n\n")
		}
		printAppDetailed(*app)
	}
}

func printAppDetailed(app fdroidcl.App) {
	p := func(title string, format string, args ...interface{}) {
		if format == "" {
			fmt.Println(title)
		} else {
			fmt.Printf("%s %s\n", title, fmt.Sprintf(format, args...))
		}
	}
	p("Package          :", "%s", app.ID)
	p("Name             :", "%s", app.Name)
	p("Summary          :", "%s", app.Summary)
	p("Current Version  :", "%s (%d)", app.CurApk.VName, app.CurApk.VCode)
	p("Upstream Version :", "%s (%d)", app.CVName, app.CVCode)
	p("License          :", "%s", app.License)
	if app.Categs != nil {
		p("Categories       :", "%s", strings.Join(app.Categs, ", "))
	}
	if app.Website != "" {
		p("Website          :", "%s", app.Website)
	}
	if app.Source != "" {
		p("Source           :", "%s", app.Source)
	}
	if app.Tracker != "" {
		p("Tracker          :", "%s", app.Tracker)
	}
	if app.Changelog != "" {
		p("Changelog        :", "%s", app.Changelog)
	}
	if app.Donate != "" {
		p("Donate           :", "%s", app.Donate)
	}
	if app.Bitcoin != "" {
		p("Bitcoin          :", "bitcoin:%s", app.Bitcoin)
	}
	if app.Litecoin != "" {
		p("Litecoin         :", "litecoin:%s", app.Litecoin)
	}
	if app.Dogecoin != "" {
		p("Dogecoin         :", "dogecoin:%s", app.Dogecoin)
	}
	if app.FlattrID != "" {
		p("Flattr           :", "https://flattr.com/thing/%s", app.FlattrID)
	}
	fmt.Println()
	p("Description :", "")
	fmt.Println()
	app.TextDesc(os.Stdout)
	fmt.Println()
	p("Available Versions :", "")
	for _, apk := range app.Apks {
		fmt.Println()
		p("    Name   :", "%s (%d)", apk.VName, apk.VCode)
		p("    Size   :", "%d", apk.Size)
		p("    MinSdk :", "%d", apk.MinSdk)
		if apk.MaxSdk > 0 {
			p("    MaxSdk :", "%d", apk.MaxSdk)
		}
		if apk.ABIs != nil {
			p("    ABIs   :", "%s", strings.Join(apk.ABIs, ", "))
		}
	}
}
<commit_msg>Reorder funcs<commit_after>/* Copyright (c) 2015, Daniel Martí <mvdan@mvdan.cc> */
/* See LICENSE for licensing information */

package main

import (
	"fmt"
	"log"
	"os"
	"strings"

	"github.com/mvdan/fdroidcl"
)

var cmdShow = &Command{
	UsageLine: "show <appid...>",
	Short:     "Show detailed info about an app",
}

func init() {
	cmdShow.Run = runShow
}

func runShow(args []string) {
	apps := findApps(args)
	for i, app := range apps {
		if app == nil {
			log.Fatalf("Could not find app with ID '%s'", args[i])
		}
		if i > 0 {
			fmt.Printf("\n--\n\n")
		}
		printAppDetailed(*app)
	}
}

func findApps(ids []string) []*fdroidcl.App {
	index := mustLoadIndex()
	apps := make(map[string]*fdroidcl.App, len(index.Apps))
	for i := range index.Apps {
		app := &index.Apps[i]
		apps[app.ID] = app
	}
	result := make([]*fdroidcl.App, len(ids))
	for i, id := range ids {
		app, _ := apps[id]
		result[i] = app
	}
	return result
}

func printAppDetailed(app fdroidcl.App) {
	p := func(title string, format string, args ...interface{}) {
		if format == "" {
			fmt.Println(title)
		} else {
			fmt.Printf("%s %s\n", title, fmt.Sprintf(format, args...))
		}
	}
	p("Package          :", "%s", app.ID)
	p("Name             :", "%s", app.Name)
	p("Summary          :", "%s", app.Summary)
	p("Current Version  :", "%s (%d)", app.CurApk.VName, app.CurApk.VCode)
	p("Upstream Version :", "%s (%d)", app.CVName, app.CVCode)
	p("License          :", "%s", app.License)
	if app.Categs != nil {
		p("Categories       :", "%s", strings.Join(app.Categs, ", "))
	}
	if app.Website != "" {
		p("Website          :", "%s", app.Website)
	}
	if app.Source != "" {
		p("Source           :", "%s", app.Source)
	}
	if app.Tracker != "" {
		p("Tracker          :", "%s", app.Tracker)
	}
	if app.Changelog != "" {
		p("Changelog        :", "%s", app.Changelog)
	}
	if app.Donate != "" {
		p("Donate           :", "%s", app.Donate)
	}
	if app.Bitcoin != "" {
		p("Bitcoin          :", "bitcoin:%s", app.Bitcoin)
	}
	if app.Litecoin != "" {
		p("Litecoin         :", "litecoin:%s", app.Litecoin)
	}
	if app.Dogecoin != "" {
		p("Dogecoin         :", "dogecoin:%s", app.Dogecoin)
	}
	if app.FlattrID != "" {
		p("Flattr           :", "https://flattr.com/thing/%s", app.FlattrID)
	}
	fmt.Println()
	p("Description :", "")
	fmt.Println()
	app.TextDesc(os.Stdout)
	fmt.Println()
	p("Available Versions :", "")
	for _, apk := range app.Apks {
		fmt.Println()
		p("    Name   :", "%s (%d)", apk.VName, apk.VCode)
		p("    Size   :", "%d", apk.Size)
		p("    MinSdk :", "%d", apk.MinSdk)
		if apk.MaxSdk > 0 {
			p("    MaxSdk :", "%d", apk.MaxSdk)
		}
		if apk.ABIs != nil {
			p("    ABIs   :", "%s", strings.Join(apk.ABIs, ", "))
		}
	}
}
<|endoftext|>


<commit_before>package dns

// A concurrent client implementation.

import (
	"io"
	"net"
	"time"
)

// hijacked connections...?
type reply struct {
	client         *Client
	addr           string
	req            *Msg
	conn           net.Conn
	tsigRequestMAC string
	tsigTimersOnly bool
	tsigStatus     error
	rtt            time.Duration
	t              time.Time
}

// A nil Client is usable.
type Client struct {
	Net          string            // if "tcp" a TCP query will be initiated, otherwise an UDP one (default is "", is UDP)
	Attempts     int               // number of attempts, if not set defaults to 1
	Retry        bool              // retry with TCP
	ReadTimeout  time.Duration     // the net.Conn.SetReadTimeout value for new connections (ns), defauls to 2 * 1e9
	WriteTimeout time.Duration     // the net.Conn.SetWriteTimeout value for new connections (ns), defauls to 2 * 1e9
	TsigSecret   map[string]string // secret(s) for Tsig map[<zonename>]<base64 secret>
}

func (w *reply) RemoteAddr() net.Addr {
	if w.conn == nil {
		return nil
	} else {
		return w.conn.RemoteAddr()
	}
	return nil
}

// Do performs an asynchronous query. The msg *Msg is the question to ask, the
// string addr is the address of the nameserver, the parameter data is used
// in the callback function. The call backback function is called with the
// original query, the answer returned from the nameserver an optional error and
// data.
func (c *Client) Do(msg *Msg, addr string, data interface{}, callback func(*Msg, *Msg, error, interface{})) {
	go func() {
		r, err := c.Exchange(msg, addr)
		callback(msg, r, err, data)
	}()
}

// DoRtt is equivalent to Do, except that is calls ExchangeRtt.
func (c *Client) DoRtt(msg *Msg, addr string, data interface{}, callback func(*Msg, *Msg, time.Duration, error, interface{})) {
	go func() {
		r, rtt, err := c.ExchangeRtt(msg, addr)
		callback(msg, r, rtt, err, data)
	}()
}

// exchangeBuffer performs a synchronous query. It sends the buffer m to the
// address contained in a.
func (c *Client) exchangeBuffer(inbuf []byte, a string, outbuf []byte) (n int, w *reply, err error) {
	w = new(reply)
	w.client = c
	w.addr = a
	// attempts useful for dialing?
	if err = w.dial(); err != nil {
		return 0, w, err
	}
	defer w.conn.Close()
	w.t = time.Now()
	if n, err = w.writeClient(inbuf); err != nil {
		return 0, w, err
	}
	if n, err = w.readClient(outbuf); err != nil {
		return n, w, err
	}
	w.rtt = time.Since(w.t)
	return n, w, nil
}

// Exchange performs an synchronous query. It sends the message m to the address
// contained in a and waits for an reply. Basic use pattern with a *Client:
//
//	c := new(dns.Client)
//	in, err := c.Exchange(message, "127.0.0.1:53")
//
// See Client.ExchangeRtt(...) to get the round trip time.
func (c *Client) Exchange(m *Msg, a string) (r *Msg, err error) {
	r, _, err = c.ExchangeRtt(m, a)
	return
}

// ExchangeRtt performs an synchronous query. It sends the message m to the address
// contained in a and waits for an reply. Basic use pattern with a *Client:
//
//	c := new(dns.Client)
//	in, rtt, err := c.ExchangeRtt(message, "127.0.0.1:53")
//
func (c *Client) ExchangeRtt(m *Msg, a string) (r *Msg, rtt time.Duration, err error) {
	var n int
	var w *reply
	out, ok := m.Pack()
	if !ok {
		return nil, 0, ErrPack
	}
	var in []byte
	switch c.Net {
	case "tcp", "tcp4", "tcp6":
		in = make([]byte, MaxMsgSize)
	case "", "udp", "udp4", "udp6":
		size := udpMsgSize
		for _, r := range m.Extra {
			if r.Header().Rrtype == TypeOPT {
				size = int(r.(*RR_OPT).UDPSize())
			}
		}
		in = make([]byte, size)
	}
	if n, w, err = c.exchangeBuffer(out, a, in); err != nil {
		return nil, 0, err
	}
	r = new(Msg)
	r.Size = n
	if ok := r.Unpack(in[:n]); !ok {
		return nil, w.rtt, ErrUnpack
	}
	return r, w.rtt, nil
}

// dial connects to the address addr for the network set in c.Net
func (w *reply) dial() (err error) {
	var conn net.Conn
	if w.client.Net == "" {
		conn, err = net.Dial("udp", w.addr)
	} else {
		conn, err = net.Dial(w.client.Net, w.addr)
	}
	if err != nil {
		return
	}
	w.conn = conn
	return nil
}

func (w *reply) receive() (*Msg, error) {
	var p []byte
	m := new(Msg)
	switch w.client.Net {
	case "tcp", "tcp4", "tcp6":
		p = make([]byte, MaxMsgSize)
	case "", "udp", "udp4", "udp6":
		p = make([]byte, DefaultMsgSize)
	}
	n, err := w.readClient(p)
	if err != nil && n == 0 {
		return nil, err
	}
	p = p[:n]
	if ok := m.Unpack(p); !ok {
		return nil, ErrUnpack
	}
	w.rtt = time.Since(w.t)
	m.Size = n
	if t := m.IsTsig(); t != nil {
		secret := t.Hdr.Name
		if _, ok := w.client.TsigSecret[secret]; !ok {
			w.tsigStatus = ErrSecret
			return m, nil
		}
		// Need to work on the original message p, as that was used to calculate the tsig.
		w.tsigStatus = TsigVerify(p, w.client.TsigSecret[secret], w.tsigRequestMAC, w.tsigTimersOnly)
	}
	return m, nil
}

func (w *reply) readClient(p []byte) (n int, err error) {
	if w.conn == nil {
		return 0, ErrConnEmpty
	}
	if len(p) < 2 {
		return 0, io.ErrShortBuffer
	}
	attempts := w.client.Attempts
	if attempts == 0 {
		attempts = 1
	}
	switch w.client.Net {
	case "tcp", "tcp4", "tcp6":
		setTimeouts(w)
		for a := 0; a < attempts; a++ {
			n, err = w.conn.(*net.TCPConn).Read(p[0:2])
			if err != nil || n != 2 {
				if e, ok := err.(net.Error); ok && e.Timeout() {
					continue
				}
				return n, err
			}
			l, _ := unpackUint16(p[0:2], 0)
			if l == 0 {
				return 0, ErrShortRead
			}
			if int(l) > len(p) {
				return int(l), io.ErrShortBuffer
			}
			n, err = w.conn.(*net.TCPConn).Read(p[:l])
			if err != nil {
				if e, ok := err.(net.Error); ok && e.Timeout() {
					continue
				}
				return n, err
			}
			i := n
			for i < int(l) {
				j, err := w.conn.(*net.TCPConn).Read(p[i:int(l)])
				if err != nil {
					if e, ok := err.(net.Error); ok && e.Timeout() {
						// We are half way in our read...
						continue
					}
					return i, err
				}
				i += j
			}
			n = i
		}
	case "", "udp", "udp4", "udp6":
		for a := 0; a < attempts; a++ {
			setTimeouts(w)
			n, _, err = w.conn.(*net.UDPConn).ReadFromUDP(p)
			if err == nil {
				return n, err
			}
			if err != nil {
				if e, ok := err.(net.Error); ok && e.Timeout() {
					continue
				}
				return n, err
			}
		}
	}
	return
}

// send sends a dns msg to the address specified in w.
// If the message m contains a TSIG record the transaction
// signature is calculated.
func (w *reply) send(m *Msg) (err error) {
	var out []byte
	if t := m.IsTsig(); t != nil {
		mac := ""
		name := t.Hdr.Name
		if _, ok := w.client.TsigSecret[name]; !ok {
			return ErrSecret
		}
		out, mac, err = TsigGenerate(m, w.client.TsigSecret[name], w.tsigRequestMAC, w.tsigTimersOnly)
		if err != nil {
			return err
		}
		w.tsigRequestMAC = mac
	} else {
		ok := false
		out, ok = m.Pack()
		if !ok {
			return ErrPack
		}
	}
	w.t = time.Now()
	if _, err = w.writeClient(out); err != nil {
		return err
	}
	return nil
}

func (w *reply) writeClient(p []byte) (n int, err error) {
	attempts := w.client.Attempts
	if attempts == 0 {
		attempts = 1
	}
	switch w.client.Net {
	case "tcp", "tcp4", "tcp6":
		if len(p) < 2 {
			return 0, io.ErrShortBuffer
		}
		for a := 0; a < attempts; a++ {
			setTimeouts(w)
			a, b := packUint16(uint16(len(p)))
			n, err = w.conn.Write([]byte{a, b})
			if err != nil {
				if e, ok := err.(net.Error); ok && e.Timeout() {
					continue
				}
				return n, err
			}
			if n != 2 {
				return n, io.ErrShortWrite
			}
			n, err = w.conn.Write(p)
			if err != nil {
				if e, ok := err.(net.Error); ok && e.Timeout() {
					continue
				}
				return n, err
			}
			i := n
			if i < len(p) {
				j, err := w.conn.Write(p[i:len(p)])
				if err != nil {
					if e, ok := err.(net.Error); ok && e.Timeout() {
						// We are half way in our write...
						continue
					}
					return i, err
				}
				i += j
			}
			n = i
		}
	case "", "udp", "udp4", "udp6":
		for a := 0; a < attempts; a++ {
			setTimeouts(w)
			n, err = w.conn.(*net.UDPConn).Write(p)
			if err == nil {
				return
			}
			if err != nil {
				if e, ok := err.(net.Error); ok && e.Timeout() {
					continue
				}
				return n, err
			}
		}
	}
	return
}

func setTimeouts(w *reply) {
	if w.client.ReadTimeout == 0 {
		w.conn.SetReadDeadline(time.Now().Add(2 * 1e9))
	} else {
		w.conn.SetReadDeadline(time.Now().Add(w.client.ReadTimeout))
	}

	if w.client.WriteTimeout == 0 {
		w.conn.SetWriteDeadline(time.Now().Add(2 * 1e9))
	} else {
		w.conn.SetWriteDeadline(time.Now().Add(w.client.WriteTimeout))
	}
}
<commit_msg>small tweaks<commit_after>package dns

// A concurrent client implementation.

import (
	"io"
	"net"
	"time"
)

type reply struct {
	client         *Client
	addr           string
	req            *Msg
	conn           net.Conn
	tsigRequestMAC string
	tsigTimersOnly bool
	tsigStatus     error
	rtt            time.Duration
	t              time.Time
}

// A Client is uses when initiating a query to a nameserver. A nil
// Client is usable.
type Client struct {
	Net          string            // if "tcp" a TCP query will be initiated, otherwise an UDP one (default is "", is UDP)
	Attempts     int               // number of attempts, if not set defaults to 1
	Retry        bool              // retry with TCP
	ReadTimeout  time.Duration     // the net.Conn.SetReadTimeout value for new connections (ns), defauls to 2 * 1e9
	WriteTimeout time.Duration     // the net.Conn.SetWriteTimeout value for new connections (ns), defauls to 2 * 1e9
	TsigSecret   map[string]string // secret(s) for Tsig map[<zonename>]<base64 secret>
}

func (w *reply) RemoteAddr() net.Addr {
	if w.conn != nil {
		return w.conn.RemoteAddr()
	}
	return nil
}

// Do performs an asynchronous query. The msg *Msg is the question to ask, the
// string addr is the address of the nameserver, the parameter data is used
// in the callback function. The call backback function is called with the
// original query, the answer returned from the nameserver an optional error and
// data.
func (c *Client) Do(msg *Msg, addr string, data interface{}, callback func(*Msg, *Msg, error, interface{})) {
	go func() {
		r, err := c.Exchange(msg, addr)
		callback(msg, r, err, data)
	}()
}

// DoRtt is equivalent to Do, except that is calls ExchangeRtt.
func (c *Client) DoRtt(msg *Msg, addr string, data interface{}, callback func(*Msg, *Msg, time.Duration, error, interface{})) {
	go func() {
		r, rtt, err := c.ExchangeRtt(msg, addr)
		callback(msg, r, rtt, err, data)
	}()
}

// exchangeBuffer performs a synchronous query. It sends the buffer m to the
// address contained in a.
func (c *Client) exchangeBuffer(inbuf []byte, a string, outbuf []byte) (n int, w *reply, err error) {
	w = new(reply)
	w.client = c
	w.addr = a
	// attempts useful for dialing?
	if err = w.dial(); err != nil {
		return 0, w, err
	}
	defer w.conn.Close()
	w.t = time.Now()
	if n, err = w.writeClient(inbuf); err != nil {
		return 0, w, err
	}
	if n, err = w.readClient(outbuf); err != nil {
		return n, w, err
	}
	w.rtt = time.Since(w.t)
	return n, w, nil
}

// Exchange performs an synchronous query. It sends the message m to the address
// contained in a and waits for an reply. Basic use pattern with a *Client:
//
//	c := new(dns.Client)
//	in, err := c.Exchange(message, "127.0.0.1:53")
//
// See Client.ExchangeRtt(...) to get the round trip time.
func (c *Client) Exchange(m *Msg, a string) (r *Msg, err error) {
	r, _, err = c.ExchangeRtt(m, a)
	return
}

// ExchangeRtt performs an synchronous query. It sends the message m to the address
// contained in a and waits for an reply. Basic use pattern with a *Client:
//
//	c := new(dns.Client)
//	in, rtt, err := c.ExchangeRtt(message, "127.0.0.1:53")
//
func (c *Client) ExchangeRtt(m *Msg, a string) (r *Msg, rtt time.Duration, err error) {
	var n int
	var w *reply
	out, ok := m.Pack()
	if !ok {
		return nil, 0, ErrPack
	}
	var in []byte
	switch c.Net {
	case "tcp", "tcp4", "tcp6":
		in = make([]byte, MaxMsgSize)
	case "", "udp", "udp4", "udp6":
		size := udpMsgSize
		for _, r := range m.Extra {
			if r.Header().Rrtype == TypeOPT {
				size = int(r.(*RR_OPT).UDPSize())
			}
		}
		in = make([]byte, size)
	}
	if n, w, err = c.exchangeBuffer(out, a, in); err != nil {
		return nil, 0, err
	}
	r = new(Msg)
	r.Size = n
	if ok := r.Unpack(in[:n]); !ok {
		return nil, w.rtt, ErrUnpack
	}
	return r, w.rtt, nil
}

// dial connects to the address addr for the network set in c.Net
func (w *reply) dial() (err error) {
	var conn net.Conn
	if w.client.Net == "" {
		conn, err = net.Dial("udp", w.addr)
	} else {
		conn, err = net.Dial(w.client.Net, w.addr)
	}
	if err != nil {
		return
	}
	w.conn = conn
	return nil
}

func (w *reply) receive() (*Msg, error) {
	var p []byte
	m := new(Msg)
	switch w.client.Net {
	case "tcp", "tcp4", "tcp6":
		p = make([]byte, MaxMsgSize)
	case "", "udp", "udp4", "udp6":
		p = make([]byte, DefaultMsgSize)
	}
	n, err := w.readClient(p)
	if err != nil && n == 0 {
		return nil, err
	}
	p = p[:n]
	if ok := m.Unpack(p); !ok {
		return nil, ErrUnpack
	}
	w.rtt = time.Since(w.t)
	m.Size = n
	if t := m.IsTsig(); t != nil {
		secret := t.Hdr.Name
		if _, ok := w.client.TsigSecret[secret]; !ok {
			w.tsigStatus = ErrSecret
			return m, nil
		}
		// Need to work on the original message p, as that was used to calculate the tsig.
		w.tsigStatus = TsigVerify(p, w.client.TsigSecret[secret], w.tsigRequestMAC, w.tsigTimersOnly)
	}
	return m, nil
}

func (w *reply) readClient(p []byte) (n int, err error) {
	if w.conn == nil {
		return 0, ErrConnEmpty
	}
	if len(p) < 2 {
		return 0, io.ErrShortBuffer
	}
	attempts := w.client.Attempts
	if attempts == 0 {
		attempts = 1
	}
	switch w.client.Net {
	case "tcp", "tcp4", "tcp6":
		setTimeouts(w)
		for a := 0; a < attempts; a++ {
			n, err = w.conn.(*net.TCPConn).Read(p[0:2])
			if err != nil || n != 2 {
				if e, ok := err.(net.Error); ok && e.Timeout() {
					continue
				}
				return n, err
			}
			l, _ := unpackUint16(p[0:2], 0)
			if l == 0 {
				return 0, ErrShortRead
			}
			if int(l) > len(p) {
				return int(l), io.ErrShortBuffer
			}
			n, err = w.conn.(*net.TCPConn).Read(p[:l])
			if err != nil {
				if e, ok := err.(net.Error); ok && e.Timeout() {
					continue
				}
				return n, err
			}
			i := n
			for i < int(l) {
				j, err := w.conn.(*net.TCPConn).Read(p[i:int(l)])
				if err != nil {
					if e, ok := err.(net.Error); ok && e.Timeout() {
						// We are half way in our read...
						continue
					}
					return i, err
				}
				i += j
			}
			n = i
		}
	case "", "udp", "udp4", "udp6":
		for a := 0; a < attempts; a++ {
			setTimeouts(w)
			n, _, err = w.conn.(*net.UDPConn).ReadFromUDP(p)
			if err == nil {
				return n, err
			}
			if err != nil {
				if e, ok := err.(net.Error); ok && e.Timeout() {
					continue
				}
				return n, err
			}
		}
	}
	return
}

// send sends a dns msg to the address specified in w.
// If the message m contains a TSIG record the transaction
// signature is calculated.
func (w *reply) send(m *Msg) (err error) {
	var out []byte
	if t := m.IsTsig(); t != nil {
		mac := ""
		name := t.Hdr.Name
		if _, ok := w.client.TsigSecret[name]; !ok {
			return ErrSecret
		}
		out, mac, err = TsigGenerate(m, w.client.TsigSecret[name], w.tsigRequestMAC, w.tsigTimersOnly)
		if err != nil {
			return err
		}
		w.tsigRequestMAC = mac
	} else {
		ok := false
		out, ok = m.Pack()
		if !ok {
			return ErrPack
		}
	}
	w.t = time.Now()
	if _, err = w.writeClient(out); err != nil {
		return err
	}
	return nil
}

func (w *reply) writeClient(p []byte) (n int, err error) {
	attempts := w.client.Attempts
	if attempts == 0 {
		attempts = 1
	}
	switch w.client.Net {
	case "tcp", "tcp4", "tcp6":
		if len(p) < 2 {
			return 0, io.ErrShortBuffer
		}
		for a := 0; a < attempts; a++ {
			setTimeouts(w)
			a, b := packUint16(uint16(len(p)))
			n, err = w.conn.Write([]byte{a, b})
			if err != nil {
				if e, ok := err.(net.Error); ok && e.Timeout() {
					continue
				}
				return n, err
			}
			if n != 2 {
				return n, io.ErrShortWrite
			}
			n, err = w.conn.Write(p)
			if err != nil {
				if e, ok := err.(net.Error); ok && e.Timeout() {
					continue
				}
				return n, err
			}
			i := n
			if i < len(p) {
				j, err := w.conn.Write(p[i:len(p)])
				if err != nil {
					if e, ok := err.(net.Error); ok && e.Timeout() {
						// We are half way in our write...
						continue
					}
					return i, err
				}
				i += j
			}
			n = i
		}
	case "", "udp", "udp4", "udp6":
		for a := 0; a < attempts; a++ {
			setTimeouts(w)
			n, err = w.conn.(*net.UDPConn).Write(p)
			if err == nil {
				return
			}
			if err != nil {
				if e, ok := err.(net.Error); ok && e.Timeout() {
					continue
				}
				return n, err
			}
		}
	}
	return
}

func setTimeouts(w *reply) {
	if w.client.ReadTimeout == 0 {
		w.conn.SetReadDeadline(time.Now().Add(2 * 1e9))
	} else {
		w.conn.SetReadDeadline(time.Now().Add(w.client.ReadTimeout))
	}

	if w.client.WriteTimeout == 0 {
		w.conn.SetWriteDeadline(time.Now().Add(2 * 1e9))
	} else {
		w.conn.SetWriteDeadline(time.Now().Add(w.client.WriteTimeout))
	}
}
<|endoftext|>


<commit_before>package rtmp

import (
	"crypto/tls"
	"github.com/elobuff/goamf"
	"net"
	"net/url"
	"sync"
	"sync/atomic"
	"time"
)

type Client struct {
	url string

	connected bool

	conn net.Conn

	outBytes        uint32
	outMessages     chan *Message
	outWindowSize   uint32
	outChunkSize    uint32
	outChunkStreams map[uint32]*OutboundChunkStream
	inBytes         uint32
	inMessages      chan *Message
	inNotify        chan uint8
	inWindowSize    uint32
	inChunkSize     uint32
	inChunkStreams  map[uint32]*InboundChunkStream

	responses      map[uint32]*Response
	responsesMutex sync.Mutex

	lastTransactionId uint32
	connectionId      string

	amfExternalHandlers map[string]amf.ExternalHandler
}

func NewClient(url string) (c *Client) {
	c = &Client{
		url:                 url,
		amfExternalHandlers: make(map[string]amf.ExternalHandler),
	}

	c.Reset()

	return
}

func (c *Client) IsAlive() bool {
	if c.connected != true {
		return false
	}

	return true
}

func (c *Client) Reset() {
	c.connected = false

	if c.conn != nil {
		c.conn.Close()
	}

	if c.outMessages != nil {
		close(c.outMessages)
	}

	if c.inMessages != nil {
		close(c.inMessages)
	}

	c.outBytes = 0
	c.outMessages = make(chan *Message, 1000)
	c.outChunkSize = DEFAULT_CHUNK_SIZE
	c.outWindowSize = DEFAULT_WINDOW_SIZE
	c.outChunkStreams = make(map[uint32]*OutboundChunkStream)
	c.inBytes = 0
	c.inMessages = make(chan *Message, 1000)
	c.inChunkSize = DEFAULT_CHUNK_SIZE
	c.inWindowSize = DEFAULT_WINDOW_SIZE
	c.inChunkStreams = make(map[uint32]*InboundChunkStream)
	c.responses = make(map[uint32]*Response)
	c.lastTransactionId = 0
	c.connectionId = ""
}

func (c *Client) RegisterExternalHandler(name string, fn amf.ExternalHandler) {
	c.amfExternalHandlers[name] = fn
}

func (c *Client) Disconnect() {
	c.Reset()
	log.Info("disconnected from %s", c.url)
}

func (c *Client) Connect() (err error) {
	log.Info("connecting to %s", c.url)

	url, err := url.Parse(c.url)
	if err != nil {
		return err
	}

	switch url.Scheme {
	case "rtmp":
		c.conn, err = net.Dial("tcp", url.Host)
	case "rtmps":
		config := &tls.Config{InsecureSkipVerify: true}
		c.conn, err = tls.Dial("tcp", url.Host, config)
	default:
		return Error("Unsupported scheme: %s", url.Scheme)
	}

	log.Debug("handshaking with %s", c.url)

	err = c.handshake()
	if err != nil {
		return Error("client connect: could not complete handshake: %s", err)
	}

	log.Debug("sending connect command to %s", c.url)

	go c.receiveLoop()
	go c.sendLoop()
	go c.routeLoop()

	var id string
	id, err = c.connect()
	if err != nil {
		return Error("client connect: could not complete connect: %s", err)
	}

	c.connected = true
	c.connectionId = id

	log.Info("connected to %s (%s)", c.url, c.connectionId)

	return
}

func (c *Client) NextTransactionId() uint32 {
	return atomic.AddUint32(&c.lastTransactionId, 1)
}

func (c *Client) Call(msg *Message, t uint32) (response *Response, err error) {
	c.outMessages <- msg

	tid := msg.TransactionId
	poll := time.Tick(time.Duration(5) * time.Millisecond)
	timeout := time.After(time.Duration(t) * time.Second)

	for {
		select {
		case <-poll:
			c.responsesMutex.Lock()
			response = c.responses[tid]
			if response != nil {
				log.Trace("client call: found response for %d", tid)
				delete(c.responses, tid)
			}
			c.responsesMutex.Unlock()

			if response != nil {
				return
			}
		case <-timeout:
			return response, Error("timed out (no response after %d seconds)", t)
		}
	}

	return
}

func (c *Client) Read(p []byte) (n int, err error) {
	n, err = c.conn.Read(p)
	c.inBytes += uint32(n)
	log.Trace("read %d", n)
	return n, err
}

func (c *Client) Write(p []byte) (n int, err error) {
	n, err = c.conn.Write(p)
	c.outBytes += uint32(n)
	log.Trace("write %d", n)
	return n, err
}
<commit_msg>Reduce i/o channel size<commit_after>package rtmp

import (
	"crypto/tls"
	"github.com/elobuff/goamf"
	"net"
	"net/url"
	"sync"
	"sync/atomic"
	"time"
)

type Client struct {
	url string

	connected bool

	conn net.Conn

	outBytes        uint32
	outMessages     chan *Message
	outWindowSize   uint32
	outChunkSize    uint32
	outChunkStreams map[uint32]*OutboundChunkStream
	inBytes         uint32
	inMessages      chan *Message
	inNotify        chan uint8
	inWindowSize    uint32
	inChunkSize     uint32
	inChunkStreams  map[uint32]*InboundChunkStream

	responses      map[uint32]*Response
	responsesMutex sync.Mutex

	lastTransactionId uint32
	connectionId      string

	amfExternalHandlers map[string]amf.ExternalHandler
}

func NewClient(url string) (c *Client) {
	c = &Client{
		url:                 url,
		amfExternalHandlers: make(map[string]amf.ExternalHandler),
	}

	c.Reset()

	return
}

func (c *Client) IsAlive() bool {
	if c.connected != true {
		return false
	}

	return true
}

func (c *Client) Reset() {
	c.connected = false

	if c.conn != nil {
		c.conn.Close()
	}

	if c.outMessages != nil {
		close(c.outMessages)
	}

	if c.inMessages != nil {
		close(c.inMessages)
	}

	c.outBytes = 0
	c.outMessages = make(chan *Message, 100)
	c.outChunkSize = DEFAULT_CHUNK_SIZE
	c.outWindowSize = DEFAULT_WINDOW_SIZE
	c.outChunkStreams = make(map[uint32]*OutboundChunkStream)
	c.inBytes = 0
	c.inMessages = make(chan *Message, 100)
	c.inChunkSize = DEFAULT_CHUNK_SIZE
	c.inWindowSize = DEFAULT_WINDOW_SIZE
	c.inChunkStreams = make(map[uint32]*InboundChunkStream)
	c.responses = make(map[uint32]*Response)
	c.lastTransactionId = 0
	c.connectionId = ""
}

func (c *Client) RegisterExternalHandler(name string, fn amf.ExternalHandler) {
	c.amfExternalHandlers[name] = fn
}

func (c *Client) Disconnect() {
	c.Reset()
	log.Info("disconnected from %s", c.url)
}

func (c *Client) Connect() (err error) {
	log.Info("connecting to %s", c.url)

	url, err := url.Parse(c.url)
	if err != nil {
		return err
	}

	switch url.Scheme {
	case "rtmp":
		c.conn, err = net.Dial("tcp", url.Host)
	case "rtmps":
		config := &tls.Config{InsecureSkipVerify: true}
		c.conn, err = tls.Dial("tcp", url.Host, config)
	default:
		return Error("Unsupported scheme: %s", url.Scheme)
	}

	log.Debug("handshaking with %s", c.url)

	err = c.handshake()
	if err != nil {
		return Error("client connect: could not complete handshake: %s", err)
	}

	log.Debug("sending connect command to %s", c.url)

	go c.receiveLoop()
	go c.sendLoop()
	go c.routeLoop()

	var id string
	id, err = c.connect()
	if err != nil {
		return Error("client connect: could not complete connect: %s", err)
	}

	c.connected = true
	c.connectionId = id

	log.Info("connected to %s (%s)", c.url, c.connectionId)

	return
}

func (c *Client) NextTransactionId() uint32 {
	return atomic.AddUint32(&c.lastTransactionId, 1)
}

func (c *Client) Call(msg *Message, t uint32) (response *Response, err error) {
	c.outMessages <- msg

	tid := msg.TransactionId
	poll := time.Tick(time.Duration(5) * time.Millisecond)
	timeout := time.After(time.Duration(t) * time.Second)

	for {
		select {
		case <-poll:
			c.responsesMutex.Lock()
			response = c.responses[tid]
			if response != nil {
				log.Trace("client call: found response for %d", tid)
				delete(c.responses, tid)
			}
			c.responsesMutex.Unlock()

			if response != nil {
				return
			}
		case <-timeout:
			return response, Error("timed out (no response after %d seconds)", t)
		}
	}

	return
}

func (c *Client) Read(p []byte) (n int, err error) {
	n, err = c.conn.Read(p)
	c.inBytes += uint32(n)
	log.Trace("read %d", n)
	return n, err
}

func (c *Client) Write(p []byte) (n int, err error) {
	n, err = c.conn.Write(p)
	c.outBytes += uint32(n)
	log.Trace("write %d", n)
	return n, err
}
<|endoftext|>


<commit_before>package main

import (
	"time"
	"log"
	"os"
	"net"
	"runtime"
)
type Client struct {
	conf Config
	lc LocalConfig
	mon *Monitoring
	graphiteAddr net.TCPAddr
	lg log.Logger
	ch chan string
	chM chan string
}

// Function takes file size and returning it as int64 in bytes
func (c Client) getFileSize(file string) int64 {
	f, err := os.Open(file)
	if err != nil {
		return 0
	}
	stat, err := f.Stat()
	f.Close()
	if err != nil {
		return 0
	}
	return stat.Size()
}
/*
 Function saves []string to file. We need it cause it make a lot of IO to save and check size of file
 After every single metric
*/
func (c Client)saveSliceToRetry(results_list []string)  {
	/*
	If size of file is bigger, than max size we will remove lines from this file,
	and will call this function again to check result and write to the file.
	Recursion:)
	 */
	f, err := os.OpenFile(c.conf.RetryFile, os.O_RDWR | os.O_CREATE | os.O_APPEND, 0600)
	defer f.Close()
	if err != nil {
		c.lg.Println("CLIENT:", err.Error())
	}
	/*
	 We are saving only amount of lines which less than c.lc.fileMetricSize
	 Of course we drop old metrics if it is
	 */

	for _, metric := range results_list {
		f.WriteString(metric+"\n")
		c.mon.saved++
	}
	c.removeOldDataFromRetryFile()
}
/*
	Function is cleaning up retry-file
	wholeFile is sorted to have newest metrics on the beginning
	So we need to keep newest metrics
 */
func (c Client) removeOldDataFromRetryFile() {
	currentLinesInFile := getSizeInLinesFromFile(c.conf.RetryFile)
	if currentLinesInFile > c.lc.fileMetricSize {
		c.lg.Printf("I can not save to %s more, than %d. I will have to drop the rest (%d)",
			c.conf.RetryFile, c.lc.fileMetricSize, currentLinesInFile-c.lc.fileMetricSize )
		// We save first c.lc.fileMetricSize of metrics (newest)
		wholeFile := readMetricsFromFile(c.conf.RetryFile)[:c.lc.fileMetricSize]
		// We need to swap our slice cause new data should be at the end
		for i, j := 0, len(wholeFile)-1; i < j; i, j = i+1, j-1 {
			wholeFile[i], wholeFile[j] = wholeFile[j], wholeFile[i]
		}
		c.saveSliceToRetry(wholeFile)
	}
}
/*
	Sending data to graphite:
	1) Metrics from monitor queue
	2) Metrics from main quere
	3) Retry file
 */
func (c Client)runClient() {
	for ;; time.Sleep(time.Duration(c.conf.ClientSendInterval) * time.Second) {
		results_list := []string{}
		readSize := len(c.chM)
		/*
			Max size of queue which we will process this run.
		*/
		maxSendQueue := c.lc.mainBufferSize + readSize

		// Get monitoring data. This must be at the beginning to avoid dropping
		for i := 0; i < readSize; i++ {
			results_list = append(results_list, <-c.chM)
		}

		/*
			We should not read more, than c.conf.MaxMetrics + c.lc.monitoringBuffSize in one try.
			Otherwise we extend our limits:
			We suppose to have limitation per minute lc.mainBufferSize + monitorMetrics,
			But if we read all buffer evey time - we multiply our value 60/c.conf.ClientSendInterval times
		*/
		readSize = len(c.ch)
		if maxSendQueue < readSize {
			readSize = maxSendQueue
		}
		for i := 0; i < readSize; i++ {
			results_list = append(results_list, <-c.ch)
		}
		/*
		If we have correct metrics in queue or retry file - we will try to connect to graphite
		If we do not have connection to graphite - we will not read cache file, which will save us a lot of CPU
		 */
		if _, err := os.Stat(c.conf.RetryFile); err == nil || len(results_list) != 0 {
			conn, err := net.DialTCP("tcp", nil, &c.graphiteAddr)
			if err != nil {
				// We can not connect to graphite - append queue in retry file
				c.lg.Println("CLIENT: can not connect to graphite server: ", err.Error())
				c.saveSliceToRetry(results_list)
			} else {
				// Check if we do not have too many metrics in buffer already
				if len(results_list) < maxSendQueue {
					// Get all data from "retry" file if there is something
					retryFileMetrics := readMetricsFromFile(c.conf.RetryFile)
					for numOfMetricFromFile, metricFromFile := range retryFileMetrics {
						if len(results_list) < maxSendQueue {
							results_list = append(results_list, metricFromFile)
							c.mon.got.retry++
						} else {
							c.lg.Printf("Can read only %d metrics from %s. Rest will be kept for the next run", numOfMetricFromFile, c.conf.RetryFile)
							c.saveSliceToRetry(retryFileMetrics[numOfMetricFromFile:])
						}
					}
				} else {
					c.lg.Println("Send buffer is full. Will not look into retry file" )
				}
				// Send metrics to graphite
				for _, metr := range results_list {
					_, err := conn.Write([]byte(metr + "\n"))
					if err != nil {
						c.lg.Println("Write to server failed:", err.Error())
						c.saveSliceToRetry([]string{metr})
						c.mon.saved++
					} else {
						c.mon.sent++
					}
				}
				if err == nil {
					conn.Close()
				}
			}
		}
		// Call gc to cleanup structures
		runtime.GC()
	}
}<commit_msg>Sending metrics - old first<commit_after>package main

import (
	"time"
	"log"
	"os"
	"net"
	"runtime"
)
type Client struct {
	conf Config
	lc LocalConfig
	mon *Monitoring
	graphiteAddr net.TCPAddr
	lg log.Logger
	ch chan string
	chM chan string
}

// Function takes file size and returning it as int64 in bytes
func (c Client) getFileSize(file string) int64 {
	f, err := os.Open(file)
	if err != nil {
		return 0
	}
	stat, err := f.Stat()
	f.Close()
	if err != nil {
		return 0
	}
	return stat.Size()
}
/*
 Function saves []string to file. We need it cause it make a lot of IO to save and check size of file
 After every single metric
*/
func (c Client)saveSliceToRetry(results_list []string)  {
	/*
	If size of file is bigger, than max size we will remove lines from this file,
	and will call this function again to check result and write to the file.
	Recursion:)
	 */
	f, err := os.OpenFile(c.conf.RetryFile, os.O_RDWR | os.O_CREATE | os.O_APPEND, 0600)
	defer f.Close()
	if err != nil {
		c.lg.Println("CLIENT:", err.Error())
	}
	/*
	 We are saving only amount of lines which less than c.lc.fileMetricSize
	 Of course we drop old metrics if it is
	 */

	for _, metric := range results_list {
		f.WriteString(metric+"\n")
		c.mon.saved++
	}
	c.removeOldDataFromRetryFile()
}
/*
	Function is cleaning up retry-file
	wholeFile is sorted to have newest metrics on the beginning
	So we need to keep newest metrics
 */
func (c Client) removeOldDataFromRetryFile() {
	currentLinesInFile := getSizeInLinesFromFile(c.conf.RetryFile)
	if currentLinesInFile > c.lc.fileMetricSize {
		c.lg.Printf("I can not save to %s more, than %d. I will have to drop the rest (%d)",
			c.conf.RetryFile, c.lc.fileMetricSize, currentLinesInFile-c.lc.fileMetricSize )
		// We save first c.lc.fileMetricSize of metrics (newest)
		wholeFile := readMetricsFromFile(c.conf.RetryFile)[:c.lc.fileMetricSize]
		// We need to swap our slice cause new data should be at the end
		for i, j := 0, len(wholeFile)-1; i < j; i, j = i+1, j-1 {
			wholeFile[i], wholeFile[j] = wholeFile[j], wholeFile[i]
		}
		c.saveSliceToRetry(wholeFile)
	}
}
/*
	Sending data to graphite:
	1) Metrics from monitor queue
	2) Metrics from main quere
	3) Retry file
 */
func (c Client)runClient() {
	for ;; time.Sleep(time.Duration(c.conf.ClientSendInterval) * time.Second) {
		results_list := []string{}
		readSize := len(c.chM)
		/*
			Max size of queue which we will process this run.
		*/
		maxSendQueue := c.lc.mainBufferSize + readSize

		// Get monitoring data. This must be at the beginning to avoid dropping
		for i := 0; i < readSize; i++ {
			results_list = append(results_list, <-c.chM)
		}

		/*
			We should not read more, than c.conf.MaxMetrics + c.lc.monitoringBuffSize in one try.
			Otherwise we extend our limits:
			We suppose to have limitation per minute lc.mainBufferSize + monitorMetrics,
			But if we read all buffer evey time - we multiply our value 60/c.conf.ClientSendInterval times
		*/
		readSize = len(c.ch)
		if maxSendQueue < readSize {
			readSize = maxSendQueue
		}
		for i := 0; i < readSize; i++ {
			results_list = append(results_list, <-c.ch)
		}
		/*
		If we have correct metrics in queue or retry file - we will try to connect to graphite
		If we do not have connection to graphite - we will not read cache file, which will save us a lot of CPU
		 */
		if _, err := os.Stat(c.conf.RetryFile); err == nil || len(results_list) != 0 {
			conn, err := net.DialTCP("tcp", nil, &c.graphiteAddr)
			if err != nil {
				// We can not connect to graphite - append queue in retry file
				c.lg.Println("CLIENT: can not connect to graphite server: ", err.Error())
				c.saveSliceToRetry(results_list)
			} else {
				// Check if we do not have too many metrics in buffer already
				if len(results_list) < maxSendQueue {
					// Get all data from "retry" file if there is something
					retryFileMetrics := readMetricsFromFile(c.conf.RetryFile)
					for numOfMetricFromFile, metricFromFile := range retryFileMetrics {
						if len(results_list) < maxSendQueue {
							results_list = append(results_list, metricFromFile)
							c.mon.got.retry++
						} else {
							c.lg.Printf("Can read only %d metrics from %s. Rest will be kept for the next run", numOfMetricFromFile, c.conf.RetryFile)
							c.saveSliceToRetry(retryFileMetrics[numOfMetricFromFile:])
						}
					}
				} else {
					c.lg.Println("Send buffer is full. Will not look into retry file" )
				}
				/*
					We need to send old metrics first, cause newer metrics may overwrite old and we definitely want to
					have newer value.
				*/
				for i := len(results_list)-1; i >= 0; i-- {
					_, err := conn.Write([]byte(results_list[i] + "\n"))
					if err != nil {
						c.lg.Println("Write to server failed:", err.Error())
						c.saveSliceToRetry([]string{results_list[i]})
						c.mon.saved++
					} else {
						c.mon.sent++
					}
				}
				if err == nil {
					conn.Close()
				}
			}
		}
		// Call gc to cleanup structures
		runtime.GC()
	}
}<|endoftext|>


<commit_before>// Copyright 2013 Francisco Souza. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package docker provides a client for the Docker remote API.
//
// See http://goo.gl/mxyql for more details on the remote API.
package docker

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"github.com/dotcloud/docker"
	"io"
	"io/ioutil"
	"net"
	"net/http"
	"net/url"
	"reflect"
	"strconv"
	"strings"
)

// ErrInvalidEndpoint is the error returned by NewClient when the given
// endpoint is invalid.
var ErrInvalidEndpoint = errors.New("Invalid endpoint")

// Client is the basic type of this package. It provides methods for
// interaction with the API.
type Client struct {
	endpoint string
	client   *http.Client
}

// NewClient returns a Client instance ready for communication with the
// given server endpoint.
func NewClient(endpoint string) (*Client, error) {
	if !isValid(endpoint) {
		return nil, ErrInvalidEndpoint
	}
	return &Client{endpoint: endpoint, client: http.DefaultClient}, nil
}

func (c *Client) do(method, path string, data interface{}) ([]byte, int, error) {
	var params io.Reader
	if data != nil {
		buf, err := json.Marshal(data)
		if err != nil {
			return nil, -1, err
		}
		params = bytes.NewBuffer(buf)
	}
	req, err := http.NewRequest(method, c.getURL(path), params)
	if err != nil {
		return nil, -1, err
	}
	req.Header.Set("User-Agent", "Docker-Client/"+docker.VERSION)
	if data != nil {
		req.Header.Set("Content-Type", "application/json")
	} else if method == "POST" {
		req.Header.Set("Content-Type", "plain/text")
	}
	resp, err := c.client.Do(req)
	if err != nil {
		if strings.Contains(err.Error(), "connection refused") {
			return nil, -1, fmt.Errorf("Can't connect to docker daemon. Is 'docker -d' running on this host?")
		}
		return nil, -1, err
	}
	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, -1, err
	}
	if resp.StatusCode < 200 || resp.StatusCode >= 400 {
		return nil, resp.StatusCode, newAPIClientError(resp.StatusCode, body)
	}
	return body, resp.StatusCode, nil
}

func (c *Client) getURL(path string) string {
	return fmt.Sprintf("%s/v%f%s", strings.TrimRight(c.endpoint, "/"), docker.API_VERSION, path)
}

func queryString(opts interface{}) string {
	if opts == nil {
		return ""
	}
	value := reflect.ValueOf(opts)
	if value.Kind() == reflect.Ptr {
		value = value.Elem()
	}
	if value.Kind() != reflect.Struct {
		return ""
	}
	items := url.Values(map[string][]string{})
	for i := 0; i < value.NumField(); i++ {
		field := value.Type().Field(i)
		key := field.Tag.Get("qs")
		if key == "" {
			key = strings.ToLower(field.Name)
		}
		v := value.Field(i)
		switch v.Kind() {
		case reflect.Bool:
			if v.Bool() {
				items.Add(key, "1")
			}
		case reflect.Int:
			fallthrough
		case reflect.Int8:
			fallthrough
		case reflect.Int16:
			fallthrough
		case reflect.Int32:
			fallthrough
		case reflect.Int64:
			if v.Int() > 0 {
				items.Add(key, strconv.FormatInt(v.Int(), 10))
			}
		case reflect.Float32:
			fallthrough
		case reflect.Float64:
			if v.Float() > 0 {
				items.Add(key, strconv.FormatFloat(v.Float(), 'f', -1, 64))
			}
		case reflect.String:
			if v.String() != "" {
				items.Add(key, v.String())
			}
		}
	}
	return items.Encode()
}

type apiClientError struct {
	status  int
	message string
}

func newAPIClientError(status int, body []byte) *apiClientError {
	return &apiClientError{status: status, message: string(body)}
}

func (e *apiClientError) Error() string {
	return fmt.Sprintf("API error (%d): %s", e.status, e.message)
}

func isValid(endpoint string) bool {
	u, err := url.Parse(endpoint)
	if err != nil {
		return false
	}
	if u.Scheme != "http" && u.Scheme != "https" {
		return false
	}
	_, port, err := net.SplitHostPort(u.Host)
	if err != nil {
		if e, ok := err.(*net.AddrError); ok {
			return e.Err == "missing port in address"
		}
		return false
	}
	number, err := strconv.ParseInt(port, 10, 64)
	return err == nil && number > 0 && number < 65536
}
<commit_msg>client: simplify switch case in queryString<commit_after>// Copyright 2013 Francisco Souza. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package docker provides a client for the Docker remote API.
//
// See http://goo.gl/mxyql for more details on the remote API.
package docker

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"github.com/dotcloud/docker"
	"io"
	"io/ioutil"
	"net"
	"net/http"
	"net/url"
	"reflect"
	"strconv"
	"strings"
)

// ErrInvalidEndpoint is the error returned by NewClient when the given
// endpoint is invalid.
var ErrInvalidEndpoint = errors.New("Invalid endpoint")

// Client is the basic type of this package. It provides methods for
// interaction with the API.
type Client struct {
	endpoint string
	client   *http.Client
}

// NewClient returns a Client instance ready for communication with the
// given server endpoint.
func NewClient(endpoint string) (*Client, error) {
	if !isValid(endpoint) {
		return nil, ErrInvalidEndpoint
	}
	return &Client{endpoint: endpoint, client: http.DefaultClient}, nil
}

func (c *Client) do(method, path string, data interface{}) ([]byte, int, error) {
	var params io.Reader
	if data != nil {
		buf, err := json.Marshal(data)
		if err != nil {
			return nil, -1, err
		}
		params = bytes.NewBuffer(buf)
	}
	req, err := http.NewRequest(method, c.getURL(path), params)
	if err != nil {
		return nil, -1, err
	}
	req.Header.Set("User-Agent", "Docker-Client/"+docker.VERSION)
	if data != nil {
		req.Header.Set("Content-Type", "application/json")
	} else if method == "POST" {
		req.Header.Set("Content-Type", "plain/text")
	}
	resp, err := c.client.Do(req)
	if err != nil {
		if strings.Contains(err.Error(), "connection refused") {
			return nil, -1, fmt.Errorf("Can't connect to docker daemon. Is 'docker -d' running on this host?")
		}
		return nil, -1, err
	}
	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, -1, err
	}
	if resp.StatusCode < 200 || resp.StatusCode >= 400 {
		return nil, resp.StatusCode, newAPIClientError(resp.StatusCode, body)
	}
	return body, resp.StatusCode, nil
}

func (c *Client) getURL(path string) string {
	return fmt.Sprintf("%s/v%f%s", strings.TrimRight(c.endpoint, "/"), docker.API_VERSION, path)
}

func queryString(opts interface{}) string {
	if opts == nil {
		return ""
	}
	value := reflect.ValueOf(opts)
	if value.Kind() == reflect.Ptr {
		value = value.Elem()
	}
	if value.Kind() != reflect.Struct {
		return ""
	}
	items := url.Values(map[string][]string{})
	for i := 0; i < value.NumField(); i++ {
		field := value.Type().Field(i)
		key := field.Tag.Get("qs")
		if key == "" {
			key = strings.ToLower(field.Name)
		}
		v := value.Field(i)
		switch v.Kind() {
		case reflect.Bool:
			if v.Bool() {
				items.Add(key, "1")
			}
		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
			if v.Int() > 0 {
				items.Add(key, strconv.FormatInt(v.Int(), 10))
			}
		case reflect.Float32, reflect.Float64:
			if v.Float() > 0 {
				items.Add(key, strconv.FormatFloat(v.Float(), 'f', -1, 64))
			}
		case reflect.String:
			if v.String() != "" {
				items.Add(key, v.String())
			}
		}
	}
	return items.Encode()
}

type apiClientError struct {
	status  int
	message string
}

func newAPIClientError(status int, body []byte) *apiClientError {
	return &apiClientError{status: status, message: string(body)}
}

func (e *apiClientError) Error() string {
	return fmt.Sprintf("API error (%d): %s", e.status, e.message)
}

func isValid(endpoint string) bool {
	u, err := url.Parse(endpoint)
	if err != nil {
		return false
	}
	if u.Scheme != "http" && u.Scheme != "https" {
		return false
	}
	_, port, err := net.SplitHostPort(u.Host)
	if err != nil {
		if e, ok := err.(*net.AddrError); ok {
			return e.Err == "missing port in address"
		}
		return false
	}
	number, err := strconv.ParseInt(port, 10, 64)
	return err == nil && number > 0 && number < 65536
}
<|endoftext|>


<commit_before>package alidayu

import (
	"crypto/hmac"
	"crypto/md5"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"net/url"
	"sort"
	"strings"
	"time"
)

const (
	// HTTP_URL is for HTTP REST API URL.
	HTTP_URL string = "http://gw.api.taobao.com/router/rest"
	// HTTPS_URL is for HTTPS REST API URL.
	HTTPS_URL string = "https://eco.taobao.com/router/rest"

	// SUCCESS_TAG_JSON is success tag for JSON in response string.
	SUCCESS_TAG_JSON string = `"success":true`
	// SUCCESS_TAG_XML is sucess tag for XML in response string
	SUCCESS_TAG_XML string = `<success>true</success>`
)

var (
	// DefCommonParams is default common parameters of alidayu APIs
	DefCommonParams = map[string]string{
		"format":      "json", // Response Format("json" or "xml")
		"v":           "2.0",  // API version("2.0")
		"sign_method": "md5",  // Sign method("md5" or "hmac")
	}
)

// Client contains app key and secret and provides method to post HTTP request like http.Request.
type Client struct {
	AppKey    string // App Key
	AppSecret string // App Secret
	UseHTTPS  bool   // Use HTTPS URL or not
	http.Client
}

// IsValid checks if a client is valid.
func (c *Client) IsValid() bool {
	if c.AppKey == "" || c.AppSecret == "" {
		return false
	}

	return true
}

// UpdateCommonParams updates the given parameters by merge default common parameters.
func (c *Client) UpdateCommonParams(params map[string]string) {
	for k, v := range DefCommonParams {
		if _, ok := params[k]; !ok {
			params[k] = v
		}
	}

	// Set App Key.
	params["app_key"] = c.AppKey

	// Set timestamp.
	t := time.Now()
	params["timestamp"] = fmt.Sprintf("%04d-%02d-%02d %02d:%02d:%02d", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second())
}

// GetSortedQueryStr sorts the keys of parameters and generate sorted query string which can be used to sign(MD5 or HMAC_MD5).
func GetSortedQueryStr(params map[string]string) (sortedQueryStr string) {
	keys := []string{}
	for k := range params {
		keys = append(keys, k)
	}

	str := ""
	sort.Strings(keys)
	for _, k := range keys {
		str += k + params[k]
	}
	return str
}

// SignMD5 gets the sorted query string and sign it using MD5.
func (c *Client) SignMD5(params map[string]string) (signature string) {
	// Sort keys by name to generate MD5 hash.
	// See details at:
	//     http://open.taobao.com/doc2/detail.htm?articleId=101617&docType=1&treeId=1
	str := fmt.Sprintf("%s%s%s", c.AppSecret, GetSortedQueryStr(params), c.AppSecret)

	return fmt.Sprintf("%X", md5.Sum([]byte(str)))

}

// SignHMAC gets the sorted query string and sign it using HMAC_MD5.
func (c *Client) SignHMAC(params map[string]string) (signature string) {
	// Sort keys by name to generate HMAC_MD5 hash.
	// See details at:
	//     http://open.taobao.com/doc2/detail.htm?articleId=101617&docType=1&treeId=1
	str := GetSortedQueryStr(params)

	// HMAC_MD5
	mac := hmac.New(md5.New, []byte(c.AppSecret))
	mac.Write([]byte(str))
	return fmt.Sprintf("%X", mac.Sum(nil))
}

// MakeRequestBody makes the HTTP request body by given parameters for each REST API.
func (c *Client) MakeRequestBody(params map[string]string) (body io.Reader, err error) {
	if !c.IsValid() {
		return nil, errors.New("Empty App Key or App Secret.")
	}

	values := url.Values{}

	// Check "method".
	if _, ok := params["method"]; !ok {
		return nil, errors.New("No method specified.")
	}

	// Update Common Params.
	c.UpdateCommonParams(params)

	// Check "format".
	if params["format"] != "json" && params["format"] != "xml" {
		return nil, fmt.Errorf("format error: %v", params["format"])
	}

	sign := ""
	switch params["sign_method"] {
	case "md5":
		sign = c.SignMD5(params)
	case "hmac":
		sign = c.SignHMAC(params)
	default:
		return nil, errors.New("Incorrect sign_method.")
	}

	params["sign"] = sign

	for k, v := range params {
		values.Set(k, v)
	}

	return strings.NewReader(values.Encode()), nil
}

// Post does the HTTP post for each REST API.
//
//   Params:
//     params: map that contains parameters of REST API. See official docs to fill the parameters.
//   Returns:
//     resp: HTTP Response. Do not forget to call resp.Body.Close() after use.
func (c *Client) Post(params map[string]string) (resp *http.Response, err error) {
	var body io.Reader

	if body, err = c.MakeRequestBody(params); err != nil {
		return nil, err
	}

	urlStr := ""
	if c.UseHTTPS {
		urlStr = HTTPS_URL
	} else {
		urlStr = HTTP_URL
	}

	req, err := http.NewRequest("POST", urlStr, body)
	if err != nil {
		return nil, err
	}

	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
	return c.Do(req)
}

// Exec executes the REST API and get the response. It's a wrapper of Post().
//
//   Params:
//     params: map that contains parameters of REST API. See official docs to fill the parameters.
//   Returns:
//     success: If REST API succeeds.
//     result: Raw response string of REST API.
func (c *Client) Exec(params map[string]string) (success bool, result string, err error) {
	resp, err := c.Post(params)
	if err != nil {
		return false, "", err
	}

	defer resp.Body.Close()
	data, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return false, "", err
	}

	result = string(data)

	switch params["format"] {
	case "json":
		success = strings.Contains(result, SUCCESS_TAG_JSON)
	case "xml":
		success = strings.Contains(result, SUCCESS_TAG_XML)
	}

	return success, result, nil
}
<commit_msg>Replace errors.New() with fmt.Errorf().<commit_after>package alidayu

import (
	"crypto/hmac"
	"crypto/md5"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"net/url"
	"sort"
	"strings"
	"time"
)

const (
	// HTTP_URL is for HTTP REST API URL.
	HTTP_URL string = "http://gw.api.taobao.com/router/rest"
	// HTTPS_URL is for HTTPS REST API URL.
	HTTPS_URL string = "https://eco.taobao.com/router/rest"

	// SUCCESS_TAG_JSON is success tag for JSON in response string.
	SUCCESS_TAG_JSON string = `"success":true`
	// SUCCESS_TAG_XML is sucess tag for XML in response string
	SUCCESS_TAG_XML string = `<success>true</success>`
)

var (
	// DefCommonParams is default common parameters of alidayu APIs
	DefCommonParams = map[string]string{
		"format":      "json", // Response Format("json" or "xml")
		"v":           "2.0",  // API version("2.0")
		"sign_method": "md5",  // Sign method("md5" or "hmac")
	}
)

// Client contains app key and secret and provides method to post HTTP request like http.Request.
type Client struct {
	AppKey    string // App Key
	AppSecret string // App Secret
	UseHTTPS  bool   // Use HTTPS URL or not
	http.Client
}

// IsValid checks if a client is valid.
func (c *Client) IsValid() bool {
	if c.AppKey == "" || c.AppSecret == "" {
		return false
	}

	return true
}

// UpdateCommonParams updates the given parameters by merge default common parameters.
func (c *Client) UpdateCommonParams(params map[string]string) {
	for k, v := range DefCommonParams {
		if _, ok := params[k]; !ok {
			params[k] = v
		}
	}

	// Set App Key.
	params["app_key"] = c.AppKey

	// Set timestamp.
	t := time.Now()
	params["timestamp"] = fmt.Sprintf("%04d-%02d-%02d %02d:%02d:%02d", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second())
}

// GetSortedQueryStr sorts the keys of parameters and generate sorted query string which can be used to sign(MD5 or HMAC_MD5).
func GetSortedQueryStr(params map[string]string) (sortedQueryStr string) {
	keys := []string{}
	for k := range params {
		keys = append(keys, k)
	}

	str := ""
	sort.Strings(keys)
	for _, k := range keys {
		str += k + params[k]
	}
	return str
}

// SignMD5 gets the sorted query string and sign it using MD5.
func (c *Client) SignMD5(params map[string]string) (signature string) {
	// Sort keys by name to generate MD5 hash.
	// See details at:
	//     http://open.taobao.com/doc2/detail.htm?articleId=101617&docType=1&treeId=1
	str := fmt.Sprintf("%s%s%s", c.AppSecret, GetSortedQueryStr(params), c.AppSecret)

	return fmt.Sprintf("%X", md5.Sum([]byte(str)))

}

// SignHMAC gets the sorted query string and sign it using HMAC_MD5.
func (c *Client) SignHMAC(params map[string]string) (signature string) {
	// Sort keys by name to generate HMAC_MD5 hash.
	// See details at:
	//     http://open.taobao.com/doc2/detail.htm?articleId=101617&docType=1&treeId=1
	str := GetSortedQueryStr(params)

	// HMAC_MD5
	mac := hmac.New(md5.New, []byte(c.AppSecret))
	mac.Write([]byte(str))
	return fmt.Sprintf("%X", mac.Sum(nil))
}

// MakeRequestBody makes the HTTP request body by given parameters for each REST API.
func (c *Client) MakeRequestBody(params map[string]string) (body io.Reader, err error) {
	if !c.IsValid() {
		return nil, fmt.Errorf("Empty App Key or App Secret.")
	}

	values := url.Values{}

	// Check "method".
	if _, ok := params["method"]; !ok {
		return nil, fmt.Errorf("No method specified.")
	}

	// Update Common Params.
	c.UpdateCommonParams(params)

	// Check "format".
	if params["format"] != "json" && params["format"] != "xml" {
		return nil, fmt.Errorf("format error: %v", params["format"])
	}

	sign := ""
	switch params["sign_method"] {
	case "md5":
		sign = c.SignMD5(params)
	case "hmac":
		sign = c.SignHMAC(params)
	default:
		return nil, fmt.Errorf("Incorrect sign_method.")
	}

	params["sign"] = sign

	for k, v := range params {
		values.Set(k, v)
	}

	return strings.NewReader(values.Encode()), nil
}

// Post does the HTTP post for each REST API.
//
//   Params:
//     params: map that contains parameters of REST API. See official docs to fill the parameters.
//   Returns:
//     resp: HTTP Response. Do not forget to call resp.Body.Close() after use.
func (c *Client) Post(params map[string]string) (resp *http.Response, err error) {
	var body io.Reader

	if body, err = c.MakeRequestBody(params); err != nil {
		return nil, err
	}

	urlStr := ""
	if c.UseHTTPS {
		urlStr = HTTPS_URL
	} else {
		urlStr = HTTP_URL
	}

	req, err := http.NewRequest("POST", urlStr, body)
	if err != nil {
		return nil, err
	}

	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
	return c.Do(req)
}

// Exec executes the REST API and get the response. It's a wrapper of Post().
//
//   Params:
//     params: map that contains parameters of REST API. See official docs to fill the parameters.
//   Returns:
//     success: If REST API succeeds.
//     result: Raw response string of REST API.
func (c *Client) Exec(params map[string]string) (success bool, result string, err error) {
	resp, err := c.Post(params)
	if err != nil {
		return false, "", err
	}

	defer resp.Body.Close()
	data, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return false, "", err
	}

	result = string(data)

	switch params["format"] {
	case "json":
		success = strings.Contains(result, SUCCESS_TAG_JSON)
	case "xml":
		success = strings.Contains(result, SUCCESS_TAG_XML)
	}

	return success, result, nil
}
<|endoftext|>


<commit_before>// Copyright (c) 2015 Pagoda Box Inc
//
// This Source Code Form is subject to the terms of the Mozilla Public License, v.
// 2.0. If a copy of the MPL was not distributed with this file, You can obtain one
// at http://mozilla.org/MPL/2.0/.
//

package mist

import (
	"encoding/binary"
	"encoding/json"
	"fmt"
	"io"
	"net"
	"strings"
	"time"

	"github.com/pagodabox/nanobox-golang-stylish"
)

//
type (

	// A Client has a connection to the mist server, a data channel from which it
	// receives messages from the server, ad a host and port to use when connecting
	// to the server
	Client struct {
		conn net.Conn     // the connection the mist server
		Data chan Message // the channel that mist server 'publishes' updates to
		Host string       // the connection host for where mist server is running
		Port string       // the connection port for where mist server is running
	}
)

// Connect attempts to connect to a running mist server at the clients specified
// host and port.
func (c *Client) Connect() error {
	fmt.Printf(stylish.Bullet("Attempting to connect to mist..."))

	// number of seconds/attempts to try when failing to conenct to mist server
	maxRetries := 60

	// attempt to connect to the host:port
	for i := 0; i < maxRetries; i++ {
		if conn, err := net.Dial("tcp", c.Host+":"+c.Port); err != nil {

			// max number of attempted retrys failed...
			if i >= maxRetries {
				fmt.Printf(stylish.Error("mist connection failed", "The attempted connection to mist failed. This shouldn't effect any running processes, however no output should be expected"))
				return err
			}
			fmt.Printf("\r   Connection failed! Retrying (%v/%v attempts)...", i, maxRetries)

			// upon successful connection, set the clients connection (conn) to the tcp
			// connection that was established with the server
		} else {
			fmt.Printf(stylish.SubBullet("- Connection established"))
			fmt.Printf(stylish.Success())
			c.conn = conn
			break
		}

		//
		time.Sleep(1 * time.Second)
	}

	// create a channel on which to publish messages received from mist server
	c.Data = make(chan Message)

	// continually read from conn, forwarding the data onto the clients data channel
	go func() {
		for {

			// read the first 4 bytes of the message so we know how long the message
			// is expected to be
			bsize := make([]byte, 4)
			if _, err := io.ReadFull(c.conn, bsize); err != nil {
				// if the connection is closed just kill the goroutine, otherwise send the
				// error along the channel
				switch err.(type) {
				case *net.OpError:
					break
				default:
					c.Data <- Message{Tags: []string{"err"}, Data: err.Error()}
				}
			}

			// create a buffer that is the length of the expected message
			n := binary.LittleEndian.Uint32(bsize)

			// read the length of the message up to the expected bytes
			b := make([]byte, n)
			if _, err := io.ReadFull(c.conn, b); err != nil {
				// if the connection is closed just kill the goroutine, otherwise send the
				// error along the channel
				switch err.(type) {
				case *net.OpError:
					break
				default:
					c.Data <- Message{Tags: []string{"err"}, Data: err.Error()}
				}
			}

			// create a new message
			msg := Message{}

			// unmarshal the raw message into a mist message
			if err := json.Unmarshal(b, &msg); err != nil {
				c.Data <- Message{Tags: []string{"err"}, Data: err.Error()}
			}

			// send the message on the client data channel to be handled from the clients
			// user
			c.Data <- msg
		}
	}()

	return nil
}

// Subscribe takes the specified tags and tells the server to subscribe to updates
// on those tags, returning the tags and an error or nil
func (c *Client) Subscribe(tags []string) ([]string, error) {
	if _, err := c.conn.Write([]byte("subscribe " + strings.Join(tags, ",") + "\n")); err != nil {
		return nil, err
	}

	return tags, nil
}

// Unsubscribe takes the specified tags and tells the server to unsubscribe from
// updates on those tags, returning an error or nil
func (c *Client) Unsubscribe(tags []string) error {
	if _, err := c.conn.Write([]byte("unsubscribe " + strings.Join(tags, ",") + "\n")); err != nil {
		return err
	}

	return nil
}

// Subscriptions requests a list of current mist subscriptions from the server
func (c *Client) Subscriptions() error {
	if _, err := c.conn.Write([]byte("subscriptions\n")); err != nil {
		return err
	}

	return nil
}

// Close closes the client data channel and the connection to the server
func (c *Client) Close() error {
	close(c.Data)
	return c.conn.Close()
}
<commit_msg>simplifying how the client handles errors until it's needs to be more robust<commit_after>// Copyright (c) 2015 Pagoda Box Inc
//
// This Source Code Form is subject to the terms of the Mozilla Public License, v.
// 2.0. If a copy of the MPL was not distributed with this file, You can obtain one
// at http://mozilla.org/MPL/2.0/.
//

package mist

import (
	"encoding/binary"
	"encoding/json"
	"fmt"
	"io"
	"net"
	"strings"
	"time"

	"github.com/pagodabox/nanobox-golang-stylish"
)

//
type (

	// A Client has a connection to the mist server, a data channel from which it
	// receives messages from the server, ad a host and port to use when connecting
	// to the server
	Client struct {
		conn net.Conn     // the connection the mist server
		Data chan Message // the channel that mist server 'publishes' updates to
		Host string       // the connection host for where mist server is running
		Port string       // the connection port for where mist server is running
	}
)

// Connect attempts to connect to a running mist server at the clients specified
// host and port.
func (c *Client) Connect() error {
	fmt.Printf(stylish.Bullet("Attempting to connect to mist..."))

	// number of seconds/attempts to try when failing to conenct to mist server
	maxRetries := 60

	// attempt to connect to the host:port
	for i := 0; i < maxRetries; i++ {
		if conn, err := net.Dial("tcp", c.Host+":"+c.Port); err != nil {

			// max number of attempted retrys failed...
			if i >= maxRetries {
				fmt.Printf(stylish.Error("mist connection failed", "The attempted connection to mist failed. This shouldn't effect any running processes, however no output should be expected"))
				return err
			}
			fmt.Printf("\r   Connection failed! Retrying (%v/%v attempts)...", i, maxRetries)

			// upon successful connection, set the clients connection (conn) to the tcp
			// connection that was established with the server
		} else {
			fmt.Printf(stylish.SubBullet("- Connection established"))
			fmt.Printf(stylish.Success())
			c.conn = conn
			break
		}

		//
		time.Sleep(1 * time.Second)
	}

	// create a channel on which to publish messages received from mist server
	c.Data = make(chan Message)

	// continually read from conn, forwarding the data onto the clients data channel
	go func() {
		for {

			// read the first 4 bytes of the message so we know how long the message
			// is expected to be
			bsize := make([]byte, 4)
			if _, err := io.ReadFull(c.conn, bsize); err != nil {

				// for now, the only err I can see causing problems here is a closed
				// connection, so for now we'll just break until we need to handle
				// different types
				break
			}

			// create a buffer that is the length of the expected message
			n := binary.LittleEndian.Uint32(bsize)

			// read the length of the message up to the expected bytes
			b := make([]byte, n)
			if _, err := io.ReadFull(c.conn, b); err != nil {

				// for now, the only err I can see causing problems here is a closed
				// connection, so for now we'll just break until we need to handle
				// different types
				break
			}

			// create a new message
			msg := Message{}

			// unmarshal the raw message into a mist message
			if err := json.Unmarshal(b, &msg); err != nil {
				c.Data <- Message{Tags: []string{"err"}, Data: err.Error()}
			}

			// send the message on the client data channel to be handled from the clients
			// user
			c.Data <- msg
		}
	}()

	return nil
}

// Subscribe takes the specified tags and tells the server to subscribe to updates
// on those tags, returning the tags and an error or nil
func (c *Client) Subscribe(tags []string) ([]string, error) {
	if _, err := c.conn.Write([]byte("subscribe " + strings.Join(tags, ",") + "\n")); err != nil {
		return nil, err
	}

	return tags, nil
}

// Unsubscribe takes the specified tags and tells the server to unsubscribe from
// updates on those tags, returning an error or nil
func (c *Client) Unsubscribe(tags []string) error {
	if _, err := c.conn.Write([]byte("unsubscribe " + strings.Join(tags, ",") + "\n")); err != nil {
		return err
	}

	return nil
}

// Subscriptions requests a list of current mist subscriptions from the server
func (c *Client) Subscriptions() error {
	if _, err := c.conn.Write([]byte("subscriptions\n")); err != nil {
		return err
	}

	return nil
}

// Close closes the client data channel and the connection to the server
func (c *Client) Close() error {
	close(c.Data)
	return c.conn.Close()
}
<|endoftext|>


<commit_before>// Copyright 2020 Tamás Gulácsi
//
//
//    Licensed under the Apache License, Version 2.0 (the "License");
//    you may not use this file except in compliance with the License.
//    You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//    Unless required by applicable law or agreed to in writing, software
//    distributed under the License is distributed on an "AS IS" BASIS,
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//    See the License for the specific language governing permissions and
//    limitations under the License.

package soapproxy

import (
	"bytes"
	"context"
	"encoding/xml"
	"io"
	"io/ioutil"
	"strconv"
	"sync"
	"time"

	errors "golang.org/x/xerrors"

	"github.com/hashicorp/go-retryablehttp"
	"github.com/tgulacsi/go/httpclient"
)

var (DefaultCallTimeout = time.Minute

clientsMu sync.RWMutex
clients = make(map[string]*retryablehttp.Client)
)

const (
	SOAPHeader = `<?xml version="1.0" encoding="UTF-8"?><soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"><soapenv:Header/><soapenv:Body>`
	SOAPFooter = `</soapenv:Body></soapenv:Envelope>`
)

// SOAPCall destURL with SOAPAction=action, decoding the response body into resp.
func SOAPCall(ctx context.Context, destURL, action string, reqBody string , resp interface{}, Log func(...interface{}) error) error {
	buf := bufPool.Get().(*bytes.Buffer)
	defer func(){
		buf.Reset()
		bufPool.Put(buf)
	}()
	buf.Write([]byte(SOAPHeader))
	io.WriteString(buf, reqBody)
	buf.Write([]byte(SOAPFooter))

	request, err := retryablehttp.NewRequest("POST", destURL, buf.Bytes())
	if err != nil {
		return err
	}
	request = request.WithContext(ctx)
	request.Header.Set("Content-Type", "text/xml; charset=utf-8")
	request.Header.Set("SOAPAction", action)
	request.Header.Set("Length", strconv.Itoa(buf.Len()))
	if Log != nil {
		Log("POST", destURL, "header", request.Header, "xml", buf.String())
	}
	clientsMu.RLock()
	cl, ok := clients[destURL]
	clientsMu.RUnlock()
	if !ok {
		clientsMu.Lock()
		if cl, ok = clients[destURL]; !ok {
			to := DefaultCallTimeout
			if dl, ok := ctx.Deadline(); ok {
				if d := time.Until(dl); d > time.Second {
					to = d
				}
			}
			cl = httpclient.NewWithClient("url="+destURL, nil, 1*time.Second, to, 0.6, Log)
			clients[destURL] = cl
		}
		clientsMu.Unlock()
	}
	response, err := cl.Do(request)
	if err != nil {
		return err
	}
	defer response.Body.Close()

	buf.Reset()
	if response.StatusCode >= 400 {
		io.Copy(buf, response.Body)
		return errors.Errorf("%s: %w", buf.String(), errors.New(response.Status))
	}

	tr := io.TeeReader(response.Body, buf)
	dec := xml.NewDecoder(tr)
	st, err := FindBody(dec)
	if err != nil {
		return err
	}
	err = dec.DecodeElement(resp, &st)
	if Log != nil {
		if err != nil {
			io.Copy(ioutil.Discard, tr)
		}
		Log("response", buf.String(), "decoded", resp, "error", err)
	}
	return err
}

<commit_msg>Make logs shorters: log only first/last 512 bytes on success<commit_after>// Copyright 2020 Tamás Gulácsi
//
//
//    Licensed under the Apache License, Version 2.0 (the "License");
//    you may not use this file except in compliance with the License.
//    You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//    Unless required by applicable law or agreed to in writing, software
//    distributed under the License is distributed on an "AS IS" BASIS,
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//    See the License for the specific language governing permissions and
//    limitations under the License.

package soapproxy

import (
	"bytes"
	"context"
	"encoding/xml"
	"fmt"
	"io"
	"io/ioutil"
	"strconv"
	"sync"
	"time"

	errors "golang.org/x/xerrors"

	"github.com/hashicorp/go-retryablehttp"
	"github.com/tgulacsi/go/httpclient"
)

var (
	DefaultCallTimeout = time.Minute

	clientsMu sync.RWMutex
	clients   = make(map[string]*retryablehttp.Client)
)

const (
	SOAPHeader = `<?xml version="1.0" encoding="UTF-8"?><soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"><soapenv:Header/><soapenv:Body>`
	SOAPFooter = `</soapenv:Body></soapenv:Envelope>`
)

// SOAPCall destURL with SOAPAction=action, decoding the response body into resp.
func SOAPCall(ctx context.Context, destURL, action string, reqBody string, resp interface{}, Log func(...interface{}) error) error {
	buf := bufPool.Get().(*bytes.Buffer)
	defer func() {
		buf.Reset()
		bufPool.Put(buf)
	}()
	buf.Write([]byte(SOAPHeader))
	io.WriteString(buf, reqBody)
	buf.Write([]byte(SOAPFooter))

	request, err := retryablehttp.NewRequest("POST", destURL, buf.Bytes())
	if err != nil {
		return err
	}
	request = request.WithContext(ctx)
	request.Header.Set("Content-Type", "text/xml; charset=utf-8")
	request.Header.Set("SOAPAction", action)
	request.Header.Set("Length", strconv.Itoa(buf.Len()))
	if Log != nil {
		Log("POST", destURL, "header", request.Header, "xml", buf.String())
	}
	clientsMu.RLock()
	cl, ok := clients[destURL]
	clientsMu.RUnlock()
	if !ok {
		clientsMu.Lock()
		if cl, ok = clients[destURL]; !ok {
			to := DefaultCallTimeout
			if dl, ok := ctx.Deadline(); ok {
				if d := time.Until(dl); d > time.Second {
					to = d
				}
			}
			cl = httpclient.NewWithClient("url="+destURL, nil, 1*time.Second, to, 0.6, Log)
			clients[destURL] = cl
		}
		clientsMu.Unlock()
	}
	response, err := cl.Do(request)
	if err != nil {
		return err
	}
	defer response.Body.Close()

	buf.Reset()
	if response.StatusCode >= 400 {
		io.Copy(buf, response.Body)
		return errors.Errorf("%s: %w", buf.String(), errors.New(response.Status))
	}

	tr := io.TeeReader(response.Body, buf)
	dec := xml.NewDecoder(tr)
	st, err := FindBody(dec)
	if err != nil {
		return err
	}
	err = dec.DecodeElement(resp, &st)
	if Log == nil {
		return err
	}
	if err != nil {
		io.Copy(ioutil.Discard, tr)
		Log("msg", "response", buf.String(), "decoded", resp, "error", err)
		return err
	}
	respLen := buf.Len()
	respHead, respTail := splitHeadTail(buf.Bytes(), 512)
	buf.Reset()
	fmt.Fprintf(buf, "%#v", resp)
	decHead, decTail := splitHeadTail(buf.Bytes(), 512)
	Log("msg", "response", "resp-length", respLen,
		"resp-head", respHead, "resp-tail", respTail,
		"decoded-length", buf.Len(), "decoded-head", decHead, "decoded-tail", decTail)
	return nil
}

func splitHeadTail(b []byte, length int) (head string, tail string) {
	if n := len(b) / 2; n <= length {
		s := string(b)
		return s[:n], s[n:]
	}
	return string(b[:length]), string(b[len(b)-length:])
}
<|endoftext|>


<commit_before>package logpackermobilesdk

import (
	"errors"
	"net/http"
)

// Client will be initialized 1 time
// ClusterURL is a host:port to the LogPacker cluster
type Client struct {
	ClusterURL  string
	Environment string
	Agent       string
}

// NewClient returns Client object and error
func NewClient(clusterURL string, environment string, agent string) (*Client, error) {
	if agent == "" {
		agent = "mobile"
	}
	if environment == "" {
		environment = "development"
	}

	c := &Client{
		ClusterURL:  clusterURL,
		Environment: environment,
		Agent:       agent,
	}

	if clusterURL == "" {
		return c, errors.New("ClusterURL must contain host:port for your LogPacker Cluster. Given " + clusterURL)
	}

	// Ping cluster Public API
	_, err := http.Get(clusterURL + "/version")
	if err != nil {
		return c, errors.New("ClusterURL " + clusterURL + " isn't reachable")
	}

	return c, nil
}
<commit_msg>Fixed error message<commit_after>package logpackermobilesdk

import (
	"errors"
	"net/http"
)

// Client will be initialized 1 time
// ClusterURL is a host:port to the LogPacker cluster
type Client struct {
	ClusterURL  string
	Environment string
	Agent       string
}

// NewClient returns Client object and error
func NewClient(clusterURL string, environment string, agent string) (*Client, error) {
	if agent == "" {
		agent = "mobile"
	}
	if environment == "" {
		environment = "development"
	}

	c := &Client{
		ClusterURL:  clusterURL,
		Environment: environment,
		Agent:       agent,
	}

	if clusterURL == "" {
		return c, errors.New("ClusterURL must contain host:port for your LogPacker Cluster")
	}

	// Ping cluster Public API
	_, err := http.Get(clusterURL + "/version")
	if err != nil {
		return c, errors.New("ClusterURL " + clusterURL + " isn't reachable")
	}

	return c, nil
}
<|endoftext|>