<commit_before>package mux

import (
	"errors"
	"fmt"
	"log"
	"sync"

	"github.com/bitfinexcom/bitfinex-api-go/pkg/mux/client"
)

// Mux will manage all connections and subscriptions. Will check if subscriptions
// limit is reached and spawn new connection when that happens. It will also listen
// to all incomming client messages and reconnect client with all its subscriptions
// in case of a failure
type Mux struct {
	CID     int
	Inbound chan client.Msg
	Clients map[int]*client.Client
	mtx     *sync.RWMutex
	Err     error
	APIKey  string
	APISec  string
}

// New returns pointer to instance of mux
func New() *Mux {
	return &Mux{
		Inbound: make(chan client.Msg),
		Clients: make(map[int]*client.Client),
		mtx:     &sync.RWMutex{},
	}
}

// Subscribe - given the details in form of hash table, subscribes client
func (m *Mux) Subscribe(sub map[string]string) *Mux {
	if m.Err != nil {
		return m
	}

	if alreadySubscribed := m.Clients[m.CID].Subs.Added(sub); alreadySubscribed {
		return m
	}

	m.Clients[m.CID].Subscribe(sub)

	if limitReached := m.Clients[m.CID].Subs.LimitReached(); limitReached {
		log.Printf("30 subs limit is reached on cID: %d, spawning new conn\n", m.CID)
		m.AddClient()
	}
	return m
}

// AddClient adds public or authenticated client depending
// on mux api keys presence
func (m *Mux) AddClient() *Mux {
	m.mtx.Lock()
	defer m.mtx.Unlock()

	if len(m.APIKey) == 0 && len(m.APISec) == 0 {
		return m.addPublicClient()
	}

	return m.addPrivateClient()
}

// Listen accepts a callback func that will get called each time mux receives a
// message from any of its clients/subscriptions. It should be called last, after
// all setup calls are made as it's blocking
func (m *Mux) Listen(cb func([]byte, error)) {
	if m.Err != nil {
		cb(nil, m.Err)
		return
	}

	log.Println("starting to listen...")

	for {
		select {
		case msg, ok := <-m.Inbound:
			log.Printf("m:%s, e:%v, chan:%t\n", msg.Msg, msg.Err, ok)
			if !ok {
				cb(nil, errors.New("channel has closed unexpectedly, restart"))
				return
			}

			if msg.Err != nil {
				cb(nil, fmt.Errorf("conn:%d has failed | err:%s | reconnecting", msg.CID, msg.Err))
				m.reconnect(msg.CID)
				continue
			}

			cb(msg.Msg, nil)
		}
	}
}

func (m *Mux) reconnect(cID int) {
	// pull old client subscriptions
	subs := m.Clients[cID].Subs.GetAll()
	// add fresh client
	m.AddClient()
	// resubscribe old events
	for subID, sub := range subs {
		log.Printf("resubscribing: %s\n", subID)
		m.Subscribe(sub)
	}
	// remove old, closed channel from the lost
	delete(m.Clients, cID)
}

func (m *Mux) addPublicClient() *Mux {
	if m.Err != nil {
		return m
	}

	// adding new client so making sure we increment cid
	m.CID++

	c := client.New(m.CID).Public()
	if c.Err != nil {
		m.Err = c.Err
		return m
	}

	m.Clients[m.CID] = c
	// start listening for incoming messages
	go c.Read(m.Inbound)
	return m
}

func (m *Mux) addPrivateClient() *Mux {
	if m.Err != nil {
		return m
	}

	// TODO: implement auth channel handler
	return m
}
<commit_msg>handing critical errors on the read stream<commit_after>package mux

import (
	"errors"
	"fmt"
	"log"
	"sync"

	"github.com/bitfinexcom/bitfinex-api-go/pkg/mux/client"
)

// Mux will manage all connections and subscriptions. Will check if subscriptions
// limit is reached and spawn new connection when that happens. It will also listen
// to all incomming client messages and reconnect client with all its subscriptions
// in case of a failure
type Mux struct {
	CID     int
	Inbound chan client.Msg
	Clients map[int]*client.Client
	mtx     *sync.RWMutex
	Err     error
	APIKey  string
	APISec  string
}

// New returns pointer to instance of mux
func New() *Mux {
	return &Mux{
		Inbound: make(chan client.Msg),
		Clients: make(map[int]*client.Client),
		mtx:     &sync.RWMutex{},
	}
}

// Subscribe - given the details in form of hash table, subscribes client
func (m *Mux) Subscribe(sub map[string]string) *Mux {
	if m.Err != nil {
		return m
	}

	if alreadySubscribed := m.Clients[m.CID].Subs.Added(sub); alreadySubscribed {
		return m
	}

	m.Clients[m.CID].Subscribe(sub)

	if limitReached := m.Clients[m.CID].Subs.LimitReached(); limitReached {
		log.Printf("30 subs limit is reached on cID: %d, spawning new conn\n", m.CID)
		m.AddClient()
	}
	return m
}

// AddClient adds public or authenticated client depending
// on mux api keys presence
func (m *Mux) AddClient() *Mux {
	m.mtx.Lock()
	defer m.mtx.Unlock()

	if len(m.APIKey) == 0 && len(m.APISec) == 0 {
		return m.addPublicClient()
	}

	return m.addPrivateClient()
}

// Listen accepts a callback func that will get called each time mux receives a
// message from any of its clients/subscriptions. It should be called last, after
// all setup calls are made as it's blocking
func (m *Mux) Listen(cb func([]byte, error)) error {
	if m.Err != nil {
		return m.Err
	}

	for {
		select {
		case msg, ok := <-m.Inbound:
			if !ok {
				return errors.New("channel has closed unexpectedly, restart")
			}

			if msg.Err != nil {
				cb(nil, fmt.Errorf("conn:%d has failed | err:%s | reconnecting", msg.CID, msg.Err))
				m.reconnect(msg.CID)
				continue
			}

			cb(msg.Msg, nil)
		}
	}
}

func (m *Mux) reconnect(cID int) {
	// pull old client subscriptions
	subs := m.Clients[cID].Subs.GetAll()
	// add fresh client
	m.AddClient()
	// resubscribe old events
	for subID, sub := range subs {
		log.Printf("resubscribing: %s\n", subID)
		m.Subscribe(sub)
	}
	// remove old, closed channel from the lost
	delete(m.Clients, cID)
}

func (m *Mux) addPublicClient() *Mux {
	if m.Err != nil {
		return m
	}

	// adding new client so making sure we increment cid
	m.CID++

	c := client.New(m.CID).Public()
	if c.Err != nil {
		m.Err = c.Err
		return m
	}

	m.Clients[m.CID] = c
	// start listening for incoming messages
	go c.Read(m.Inbound)
	return m
}

func (m *Mux) addPrivateClient() *Mux {
	if m.Err != nil {
		return m
	}

	// TODO: implement auth channel handler
	return m
}
<|endoftext|>


<commit_before>// Copyright (c) 2020, the Drone Plugins project authors.
// Please see the AUTHORS file for details. All rights reserved.
// Use of this source code is governed by an Apache 2.0 license that can be
// found in the LICENSE file.

package plugin

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/url"
	"os"
	"os/exec"
	"os/user"
	"path"
	"strings"

	"github.com/sirupsen/logrus"
)

type (
	// Settings for the Plugin.
	Settings struct {
		Username              string
		Password              string
		Token                 string
		Email                 string
		Registry              string
		Folder                string
		FailOnVersionConflict bool
		Tag                   string
		Access                string

		npm *npmPackage
	}

	npmPackage struct {
		Name    string    `json:"name"`
		Version string    `json:"version"`
		Config  npmConfig `json:"publishConfig"`
	}

	npmConfig struct {
		Registry string `json:"registry"`
	}
)

// globalRegistry defines the default NPM registry.
const globalRegistry = "https://registry.npmjs.org/"

// Validate handles the settings validation of the plugin.
func (p *Plugin) Validate() error {
	// Check authentication options
	if len(p.settings.Token) == 0 {
		if len(p.settings.Username) == 0 {
			return fmt.Errorf("No username provided")
		}
		if len(p.settings.Email) == 0 {
			return fmt.Errorf("No email address provided")
		}
		if len(p.settings.Password) == 0 {
			return fmt.Errorf("No password provided")
		}

		logrus.WithFields(logrus.Fields{
			"username": p.settings.Username,
			"email":    p.settings.Email,
		}).Info("Specified credentials")
	} else {
		logrus.Info("Token credentials being used")
	}

	// Verify package.json file
	npm, err := readPackageFile(p.settings.Folder)
	if err != nil {
		return fmt.Errorf("Invalid package.json %w", err)
	}

	// Verify the same registry is being used
	if len(p.settings.Registry) == 0 {
		p.settings.Registry = globalRegistry
	}

	if strings.Compare(p.settings.Registry, npm.Config.Registry) != 0 {
		return fmt.Errorf("Registry values do not match .drone.yml: %s package.json: %s", p.settings.Registry, npm.Config.Registry)
	}

	p.settings.npm = npm

	return nil
}

// Execute provides the implementation of the plugin.
func (p *Plugin) Execute() error {
	// Write the npmrc file
	if err := p.writeNpmrc(); err != nil {
		return fmt.Errorf("Could not create npmrc %w", err)
	}

	// Attempt authentication
	if err := p.authenticate(); err != nil {
		return fmt.Errorf("Could not authenticate %w", err)
	}

	// Determine whether to publish
	publish, err := p.shouldPublishPackage()

	if err != nil {
		return fmt.Errorf("Could not determine if package should be published %w", err)
	}

	if publish {
		logrus.Info("Publishing package")
		if err = runCommand(publishCommand(p.settings), p.settings.Folder); err != nil {
			return fmt.Errorf("Could not publish package %w", err)
		}
	} else {
		logrus.Info("Not publishing package")
	}

	return nil
}

/// writeNpmrc creates a .npmrc in the folder for authentication
func (p *Plugin) writeNpmrc() error {
	var f func(settings Settings) string
	if len(p.settings.Token) == 0 {
		logrus.WithFields(logrus.Fields{
			"username": p.settings.Username,
			"email":    p.settings.Email,
		}).Info("Specified credentials")
		f = npmrcContentsUsernamePassword
	} else {
		logrus.Info("Token credentials being used")
		f = npmrcContentsToken
	}

	// write npmrc file
	home := "/root"
	user, err := user.Current()
	if err == nil {
		home = user.HomeDir
	}
	npmrcPath := path.Join(home, ".npmrc")

	logrus.WithField("path", npmrcPath).Info("Writing npmrc")

	return ioutil.WriteFile(npmrcPath, []byte(f(p.settings)), 0644)
}

/// shouldPublishPackage determines if the package should be published
func (p *Plugin) shouldPublishPackage() (bool, error) {
	cmd := packageVersionsCommand(p.settings.npm.Name)
	cmd.Dir = p.settings.Folder

	trace(cmd)
	out, err := cmd.CombinedOutput()

	// see if there was an error
	// if there is an error its likely due to the package never being published
	if err == nil {
		// parse the json output
		var versions []string
		err = json.Unmarshal(out, &versions)

		if err != nil {
			logrus.Debug("Could not parse into array of string. Likely single value")

			var version string
			err := json.Unmarshal(out, &version)

			if err != nil {
				return false, err
			}

			versions = append(versions, version)
		}

		for _, value := range versions {
			logrus.WithField("version", value).Debug("Found version of package")

			if strings.Compare(p.settings.npm.Version, value) == 0 {
				logrus.Info("Version found in the registry")
				if p.settings.FailOnVersionConflict {
					return false, fmt.Errorf("Cannot publish package due to version conflict")
				}
				return false, nil
			}
		}

		logrus.Info("Version not found in the registry")
	} else {
		logrus.Info("Name was not found in the registry")
	}

	return true, nil
}

/// authenticate atempts to authenticate with the NPM registry.
func (p *Plugin) authenticate() error {
	var cmds []*exec.Cmd

	// Write the version command
	cmds = append(cmds, versionCommand())

	// write registry command
	if p.settings.Registry != globalRegistry {
		cmds = append(cmds, registryCommand(p.settings.Registry))
	}

	// Write auth command
	cmds = append(cmds, alwaysAuthCommand())

	// Write skip verify command
	if p.network.SkipVerify {
		cmds = append(cmds, skipVerifyCommand())
	}

	// Write whoami command to verify credentials
	cmds = append(cmds, whoamiCommand())

	// Run commands
	err := runCommands(cmds, p.settings.Folder)

	if err != nil {
		return err
	}

	return nil
}

/// readPackageFile reads the package file at the given path.
func readPackageFile(folder string) (*npmPackage, error) {
	// Verify package.json file exists
	packagePath := path.Join(folder, "package.json")
	info, err := os.Stat(packagePath)

	if os.IsNotExist(err) {
		return nil, fmt.Errorf("No package.json at %s %w", packagePath, err)
	}
	if info.IsDir() {
		return nil, fmt.Errorf("The package.json at %s is a directory", packagePath)
	}

	// Read the file
	file, err := ioutil.ReadFile(packagePath)
	if err != nil {
		return nil, fmt.Errorf("Could not read package.json at %s %w", packagePath, err)
	}

	// Unmarshal the json data
	npm := npmPackage{}
	err = json.Unmarshal(file, &npm)
	if err != nil {
		return nil, err
	}

	// Make sure values are present
	if len(npm.Name) == 0 {
		return nil, fmt.Errorf("No package name present")
	}
	if len(npm.Version) == 0 {
		return nil, fmt.Errorf("No package version present")
	}

	// Set the default registry
	if len(npm.Config.Registry) == 0 {
		npm.Config.Registry = globalRegistry
	}

	logrus.WithFields(logrus.Fields{
		"name":    npm.Name,
		"version": npm.Version,
		"path":    packagePath,
	}).Info("Found package.json")

	return &npm, nil
}

// npmrcContentsUsernamePassword creates the contents from a username and
// password
func npmrcContentsUsernamePassword(config Settings) string {
	// get the base64 encoded string
	authString := fmt.Sprintf("%s:%s", config.Username, config.Password)
	encoded := base64.StdEncoding.EncodeToString([]byte(authString))

	// create the file contents
	return fmt.Sprintf("_auth = %s\nemail = %s", encoded, config.Email)
}

/// Writes npmrc contents when using a token
func npmrcContentsToken(config Settings) string {
	registry, _ := url.Parse(config.Registry)
	registry.Scheme = "" // Reset the scheme to empty. This makes it so we will get a protocol relative URL.
	registryString := registry.String()

	if !strings.HasSuffix(registryString, "/") {
		registryString = registryString + "/"
	}
	return fmt.Sprintf("%s:_authToken=%s", registryString, config.Token)
}

// versionCommand gets the npm version
func versionCommand() *exec.Cmd {
	return exec.Command("npm", "--version")
}

// registryCommand sets the NPM registry.
func registryCommand(registry string) *exec.Cmd {
	return exec.Command("npm", "config", "set", "registry", registry)
}

// alwaysAuthCommand forces authentication.
func alwaysAuthCommand() *exec.Cmd {
	return exec.Command("npm", "config", "set", "always-auth", "true")
}

// skipVerifyCommand disables ssl verification.
func skipVerifyCommand() *exec.Cmd {
	return exec.Command("npm", "config", "set", "strict-ssl", "false")
}

// whoamiCommand creates a command that gets the currently logged in user.
func whoamiCommand() *exec.Cmd {
	return exec.Command("npm", "whoami")
}

// packageVersionsCommand gets the versions of the npm package.
func packageVersionsCommand(name string) *exec.Cmd {
	return exec.Command("npm", "view", name, "versions", "--json")
}

// publishCommand runs the publish command
func publishCommand(settings Settings) *exec.Cmd {
	commandArgs := []string{"publish"}

	if len(settings.Tag) != 0 {
		commandArgs = append(commandArgs, "--tag", settings.Tag)
	}

	if len(settings.Access) != 0 {
		commandArgs = append(commandArgs, "--access", settings.Access)
	}

	return exec.Command("npm", commandArgs...)
}

// trace writes each command to standard error (preceded by a ‘$ ’) before it
// is executed. Used for debugging your build.
func trace(cmd *exec.Cmd) {
	fmt.Fprintf(os.Stdout, "+ %s\n", strings.Join(cmd.Args, " "))
}

// runCommands executes the list of cmds in the given directory.
func runCommands(cmds []*exec.Cmd, dir string) error {
	for _, cmd := range cmds {
		err := runCommand(cmd, dir)

		if err != nil {
			return err
		}
	}

	return nil
}

func runCommand(cmd *exec.Cmd, dir string) error {
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	cmd.Dir = dir
	trace(cmd)

	return cmd.Run()
}
<commit_msg>Fix error string lint<commit_after>// Copyright (c) 2020, the Drone Plugins project authors.
// Please see the AUTHORS file for details. All rights reserved.
// Use of this source code is governed by an Apache 2.0 license that can be
// found in the LICENSE file.

package plugin

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/url"
	"os"
	"os/exec"
	"os/user"
	"path"
	"strings"

	"github.com/sirupsen/logrus"
)

type (
	// Settings for the Plugin.
	Settings struct {
		Username              string
		Password              string
		Token                 string
		Email                 string
		Registry              string
		Folder                string
		FailOnVersionConflict bool
		Tag                   string
		Access                string

		npm *npmPackage
	}

	npmPackage struct {
		Name    string    `json:"name"`
		Version string    `json:"version"`
		Config  npmConfig `json:"publishConfig"`
	}

	npmConfig struct {
		Registry string `json:"registry"`
	}
)

// globalRegistry defines the default NPM registry.
const globalRegistry = "https://registry.npmjs.org/"

// Validate handles the settings validation of the plugin.
func (p *Plugin) Validate() error {
	// Check authentication options
	if len(p.settings.Token) == 0 {
		if len(p.settings.Username) == 0 {
			return fmt.Errorf("no username provided")
		}
		if len(p.settings.Email) == 0 {
			return fmt.Errorf("no email address provided")
		}
		if len(p.settings.Password) == 0 {
			return fmt.Errorf("no password provided")
		}

		logrus.WithFields(logrus.Fields{
			"username": p.settings.Username,
			"email":    p.settings.Email,
		}).Info("Specified credentials")
	} else {
		logrus.Info("Token credentials being used")
	}

	// Verify package.json file
	npm, err := readPackageFile(p.settings.Folder)
	if err != nil {
		return fmt.Errorf("invalid package.json: %w", err)
	}

	// Verify the same registry is being used
	if len(p.settings.Registry) == 0 {
		p.settings.Registry = globalRegistry
	}

	if strings.Compare(p.settings.Registry, npm.Config.Registry) != 0 {
		return fmt.Errorf("registry values do not match .drone.yml: %s package.json: %s", p.settings.Registry, npm.Config.Registry)
	}

	p.settings.npm = npm

	return nil
}

// Execute provides the implementation of the plugin.
func (p *Plugin) Execute() error {
	// Write the npmrc file
	if err := p.writeNpmrc(); err != nil {
		return fmt.Errorf("could not create npmrc: %w", err)
	}

	// Attempt authentication
	if err := p.authenticate(); err != nil {
		return fmt.Errorf("could not authenticate: %w", err)
	}

	// Determine whether to publish
	publish, err := p.shouldPublishPackage()

	if err != nil {
		return fmt.Errorf("could not determine if package should be published: %w", err)
	}

	if publish {
		logrus.Info("Publishing package")
		if err = runCommand(publishCommand(p.settings), p.settings.Folder); err != nil {
			return fmt.Errorf("could not publish package: %w", err)
		}
	} else {
		logrus.Info("Not publishing package")
	}

	return nil
}

/// writeNpmrc creates a .npmrc in the folder for authentication
func (p *Plugin) writeNpmrc() error {
	var f func(settings Settings) string
	if len(p.settings.Token) == 0 {
		logrus.WithFields(logrus.Fields{
			"username": p.settings.Username,
			"email":    p.settings.Email,
		}).Info("Specified credentials")
		f = npmrcContentsUsernamePassword
	} else {
		logrus.Info("Token credentials being used")
		f = npmrcContentsToken
	}

	// write npmrc file
	home := "/root"
	user, err := user.Current()
	if err == nil {
		home = user.HomeDir
	}
	npmrcPath := path.Join(home, ".npmrc")

	logrus.WithField("path", npmrcPath).Info("Writing npmrc")

	return ioutil.WriteFile(npmrcPath, []byte(f(p.settings)), 0644)
}

/// shouldPublishPackage determines if the package should be published
func (p *Plugin) shouldPublishPackage() (bool, error) {
	cmd := packageVersionsCommand(p.settings.npm.Name)
	cmd.Dir = p.settings.Folder

	trace(cmd)
	out, err := cmd.CombinedOutput()

	// see if there was an error
	// if there is an error its likely due to the package never being published
	if err == nil {
		// parse the json output
		var versions []string
		err = json.Unmarshal(out, &versions)

		if err != nil {
			logrus.Debug("Could not parse into array of string. Likely single value")

			var version string
			err := json.Unmarshal(out, &version)

			if err != nil {
				return false, err
			}

			versions = append(versions, version)
		}

		for _, value := range versions {
			logrus.WithField("version", value).Debug("Found version of package")

			if strings.Compare(p.settings.npm.Version, value) == 0 {
				logrus.Info("Version found in the registry")
				if p.settings.FailOnVersionConflict {
					return false, fmt.Errorf("cannot publish package due to version conflict")
				}
				return false, nil
			}
		}

		logrus.Info("Version not found in the registry")
	} else {
		logrus.Info("Name was not found in the registry")
	}

	return true, nil
}

/// authenticate atempts to authenticate with the NPM registry.
func (p *Plugin) authenticate() error {
	var cmds []*exec.Cmd

	// Write the version command
	cmds = append(cmds, versionCommand())

	// write registry command
	if p.settings.Registry != globalRegistry {
		cmds = append(cmds, registryCommand(p.settings.Registry))
	}

	// Write auth command
	cmds = append(cmds, alwaysAuthCommand())

	// Write skip verify command
	if p.network.SkipVerify {
		cmds = append(cmds, skipVerifyCommand())
	}

	// Write whoami command to verify credentials
	cmds = append(cmds, whoamiCommand())

	// Run commands
	err := runCommands(cmds, p.settings.Folder)

	if err != nil {
		return err
	}

	return nil
}

/// readPackageFile reads the package file at the given path.
func readPackageFile(folder string) (*npmPackage, error) {
	// Verify package.json file exists
	packagePath := path.Join(folder, "package.json")
	info, err := os.Stat(packagePath)

	if os.IsNotExist(err) {
		return nil, fmt.Errorf("no package.json at %s: %w", packagePath, err)
	}
	if info.IsDir() {
		return nil, fmt.Errorf("the package.json at %s is a directory", packagePath)
	}

	// Read the file
	file, err := ioutil.ReadFile(packagePath)
	if err != nil {
		return nil, fmt.Errorf("could not read package.json at %s: %w", packagePath, err)
	}

	// Unmarshal the json data
	npm := npmPackage{}
	err = json.Unmarshal(file, &npm)
	if err != nil {
		return nil, err
	}

	// Make sure values are present
	if len(npm.Name) == 0 {
		return nil, fmt.Errorf("no package name present")
	}
	if len(npm.Version) == 0 {
		return nil, fmt.Errorf("no package version present")
	}

	// Set the default registry
	if len(npm.Config.Registry) == 0 {
		npm.Config.Registry = globalRegistry
	}

	logrus.WithFields(logrus.Fields{
		"name":    npm.Name,
		"version": npm.Version,
		"path":    packagePath,
	}).Info("Found package.json")

	return &npm, nil
}

// npmrcContentsUsernamePassword creates the contents from a username and
// password
func npmrcContentsUsernamePassword(config Settings) string {
	// get the base64 encoded string
	authString := fmt.Sprintf("%s:%s", config.Username, config.Password)
	encoded := base64.StdEncoding.EncodeToString([]byte(authString))

	// create the file contents
	return fmt.Sprintf("_auth = %s\nemail = %s", encoded, config.Email)
}

/// Writes npmrc contents when using a token
func npmrcContentsToken(config Settings) string {
	registry, _ := url.Parse(config.Registry)
	registry.Scheme = "" // Reset the scheme to empty. This makes it so we will get a protocol relative URL.
	registryString := registry.String()

	if !strings.HasSuffix(registryString, "/") {
		registryString = registryString + "/"
	}
	return fmt.Sprintf("%s:_authToken=%s", registryString, config.Token)
}

// versionCommand gets the npm version
func versionCommand() *exec.Cmd {
	return exec.Command("npm", "--version")
}

// registryCommand sets the NPM registry.
func registryCommand(registry string) *exec.Cmd {
	return exec.Command("npm", "config", "set", "registry", registry)
}

// alwaysAuthCommand forces authentication.
func alwaysAuthCommand() *exec.Cmd {
	return exec.Command("npm", "config", "set", "always-auth", "true")
}

// skipVerifyCommand disables ssl verification.
func skipVerifyCommand() *exec.Cmd {
	return exec.Command("npm", "config", "set", "strict-ssl", "false")
}

// whoamiCommand creates a command that gets the currently logged in user.
func whoamiCommand() *exec.Cmd {
	return exec.Command("npm", "whoami")
}

// packageVersionsCommand gets the versions of the npm package.
func packageVersionsCommand(name string) *exec.Cmd {
	return exec.Command("npm", "view", name, "versions", "--json")
}

// publishCommand runs the publish command
func publishCommand(settings Settings) *exec.Cmd {
	commandArgs := []string{"publish"}

	if len(settings.Tag) != 0 {
		commandArgs = append(commandArgs, "--tag", settings.Tag)
	}

	if len(settings.Access) != 0 {
		commandArgs = append(commandArgs, "--access", settings.Access)
	}

	return exec.Command("npm", commandArgs...)
}

// trace writes each command to standard error (preceded by a ‘$ ’) before it
// is executed. Used for debugging your build.
func trace(cmd *exec.Cmd) {
	fmt.Fprintf(os.Stdout, "+ %s\n", strings.Join(cmd.Args, " "))
}

// runCommands executes the list of cmds in the given directory.
func runCommands(cmds []*exec.Cmd, dir string) error {
	for _, cmd := range cmds {
		err := runCommand(cmd, dir)

		if err != nil {
			return err
		}
	}

	return nil
}

func runCommand(cmd *exec.Cmd, dir string) error {
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	cmd.Dir = dir
	trace(cmd)

	return cmd.Run()
}
<|endoftext|>


<commit_before>package main

import (
	"io/ioutil"
	"log"
	"testing"
)

func init() {
	registry = mock
	log.SetOutput(ioutil.Discard)
}

func TestProcessLineValue(t *testing.T) {
	environment = make(map[string][]string)
	currentVariable = "TESTING"
	addCurrent("")

	processLine(" value # comment")

	expected := "value"
	actual := environment[currentVariable][0]

	if actual != expected {
		t.Errorf("Expected: %q, was: %q", expected, actual)
	}
}

func TestProcessLineSection(t *testing.T) {
	environment = make(map[string][]string)

	processLine("[ A SECTION ]")

	expected := "ASECTION"
	actual := currentVariable

	if actual != expected {
		t.Errorf("Expected: %q, was: %q", expected, actual)
	}
}

func TestProcessTestFile(t *testing.T) {
	setEnv(HKLM, `data/setvar.txt`)

	expected := "valueline1"
	actual := mock.env["POKE_SECTION"]

	if actual != expected {
		t.Errorf("Expected: %q, was: %q", expected, actual)
	}
}
<commit_msg>refactor test<commit_after>package main

import (
	"io/ioutil"
	"log"
	"testing"
)

func init() {
	registry = mock
	log.SetOutput(ioutil.Discard)
}

func TestProcessLineValue(t *testing.T) {
	environment = make(map[string][]string)
	currentVariable = "TESTING"
	addCurrent("")

	processLine(" value # comment")
	assertEquals(t, "value", environment[currentVariable][0])
}

func TestProcessLineSection(t *testing.T) {
	environment = make(map[string][]string)
	processLine("[ A SECTION ]")
	assertEquals(t, "ASECTION", currentVariable)
}

func TestProcessTestFile(t *testing.T) {
	setEnv(HKLM, `data/setvar.txt`)
	assertEquals(t, "valueline1", mock.env["POKE_SECTION"])
}

func assertEquals(t *testing.T, expected string, actual string) {
	if actual != expected {
		t.Errorf("Expected: %q, was: %q", expected, actual)
	}
}
<|endoftext|>


<commit_before>package proxy

import (
	"crypto/tls"
	"encoding/json"
	"errors"
	"fmt"
	"net"
	"strconv"
	"sync"
	"unicode/utf8"

	loggingclient "code.cloudfoundry.org/diego-logging-client"
	"code.cloudfoundry.org/diego-ssh/helpers"
	"code.cloudfoundry.org/lager"
	"golang.org/x/crypto/ssh"
)

const (
	sshConnectionsMetric = "ssh-connections"
)

type Waiter interface {
	Wait() error
}

type TargetConfig struct {
	Address             string `json:"address"`
	TLSAddress          string `json:"tls_address"`
	ServerCertDomainSAN string `json:"server_cert_domain_san"`
	HostFingerprint     string `json:"host_fingerprint"`
	User                string `json:"user,omitempty"`
	Password            string `json:"password,omitempty"`
	PrivateKey          string `json:"private_key,omitempty"`
}

type LogMessage struct {
	Guid    string `json:"guid"`
	Message string `json:"message"`
	Index   int    `json:"index"`
}

type Proxy struct {
	logger       lager.Logger
	serverConfig *ssh.ServerConfig

	connectionLock *sync.Mutex
	connections    int
	metronClient   loggingclient.IngressClient

	tlsConfig *tls.Config
}

func New(
	logger lager.Logger,
	serverConfig *ssh.ServerConfig,
	metronClient loggingclient.IngressClient,
	tlsConfig *tls.Config,
) *Proxy {
	return &Proxy{
		logger:         logger,
		serverConfig:   serverConfig,
		connectionLock: &sync.Mutex{},
		metronClient:   metronClient,
		tlsConfig:      tlsConfig,
	}
}

func (p *Proxy) HandleConnection(netConn net.Conn) {
	logger := p.logger.Session("handle-connection")
	defer netConn.Close()

	serverConn, serverChannels, serverRequests, err := ssh.NewServerConn(netConn, p.serverConfig)
	if err != nil {
		return
	}
	defer serverConn.Close()

	clientConn, clientChannels, clientRequests, err := NewClientConn(logger, serverConn.Permissions, p.tlsConfig)
	if err != nil {
		return
	}

	logMessage := extractLogMessage(logger, serverConn.Permissions)

	defer func() {
		if logMessage != nil {
			endMessage := fmt.Sprintf("Remote access ended for %s", serverConn.RemoteAddr().String())
			p.metronClient.SendAppLog(logMessage.Guid, endMessage, "SSH", strconv.Itoa(logMessage.Index))
		}
		clientConn.Close()
	}()

	if logMessage != nil {
		p.metronClient.SendAppLog(logMessage.Guid, logMessage.Message, "SSH", strconv.Itoa(logMessage.Index))
	}

	fromClientLogger := logger.Session("from-client")
	fromDaemonLogger := logger.Session("from-daemon")

	go ProxyGlobalRequests(fromClientLogger, clientConn, serverRequests)
	go ProxyGlobalRequests(fromDaemonLogger, serverConn, clientRequests)

	go ProxyChannels(fromClientLogger, clientConn, serverChannels)
	go ProxyChannels(fromDaemonLogger, serverConn, clientChannels)

	p.connectionLock.Lock()
	p.connections++
	err = p.metronClient.SendMetric(sshConnectionsMetric, p.connections)
	if err != nil {
		logger.Error("failed-to-send-ssh-connections-metric", err)
	}
	p.connectionLock.Unlock()

	defer func() {
		p.emitConnectionClosing(logger)
	}()

	Wait(logger, serverConn, clientConn)
}

func (p *Proxy) emitConnectionClosing(logger lager.Logger) {
	p.connectionLock.Lock()
	p.connections--
	err := p.metronClient.SendMetric(sshConnectionsMetric, p.connections)
	p.connectionLock.Unlock()

	if err != nil {
		logger.Error("failed-to-send-ssh-connections-metric", err)
	}
}

func extractLogMessage(logger lager.Logger, perms *ssh.Permissions) *LogMessage {
	logMessageJson := perms.CriticalOptions["log-message"]
	if logMessageJson == "" {
		return nil
	}

	logMessage := &LogMessage{}
	err := json.Unmarshal([]byte(logMessageJson), logMessage)
	if err != nil {
		logger.Error("json-unmarshal-failed", err)
		return nil
	}

	return logMessage
}

func ProxyGlobalRequests(logger lager.Logger, conn ssh.Conn, reqs <-chan *ssh.Request) {
	logger = logger.Session("proxy-global-requests")

	logger.Info("started")
	defer logger.Info("completed")

	for req := range reqs {
		logger.Info("request", lager.Data{
			"type":      req.Type,
			"wantReply": req.WantReply,
			"payload":   req.Payload,
		})

		success, reply, err := conn.SendRequest(req.Type, req.WantReply, req.Payload)
		if err != nil {
			logger.Error("send-request-failed", err)
			continue
		}

		if req.WantReply {
			req.Reply(success, reply)
		}
	}
}

func ProxyChannels(logger lager.Logger, conn ssh.Conn, channels <-chan ssh.NewChannel) {
	logger = logger.Session("proxy-channels")

	logger.Info("started")
	defer func() {
		logger.Info("completed")
		conn.Close()
	}()

	for newChannel := range channels {
		handleNewChannel(logger, conn, newChannel)
	}
}

func handleNewChannel(logger lager.Logger, conn ssh.Conn, newChannel ssh.NewChannel) {
	logger.Info("new-channel", lager.Data{
		"channelType": newChannel.ChannelType(),
		"extraData":   newChannel.ExtraData(),
	})

	logger.Debug("openning-channel-to-daemon")

	targetChan, targetReqs, err := conn.OpenChannel(newChannel.ChannelType(), newChannel.ExtraData())
	if err != nil {
		logger.Error("failed-to-open-channel", err)
		if openErr, ok := err.(*ssh.OpenChannelError); ok {
			newChannel.Reject(openErr.Reason, openErr.Message)
		} else {
			newChannel.Reject(ssh.ConnectionFailed, err.Error())
		}
		return
	}
	logger.Debug("opened-channel-to-daemon")

	sourceChan, sourceReqs, err := newChannel.Accept()
	if err != nil {
		targetChan.Close()
		return
	}
	logger.Debug("accepted-channel-from-client")

	toTargetLogger := logger.Session("to-target")
	toSourceLogger := logger.Session("to-source")

	targetWg := &sync.WaitGroup{}
	sourceWg := &sync.WaitGroup{}

	targetWg.Add(2)
	go helpers.Copy(toTargetLogger.Session("stdout"), targetWg, targetChan, sourceChan)
	go helpers.Copy(toTargetLogger.Session("stderr"), targetWg, targetChan.Stderr(), sourceChan.Stderr())
	go func() {
		targetWg.Wait()
		targetChan.CloseWrite()
	}()

	sourceWg.Add(2)
	go helpers.Copy(toSourceLogger.Session("stdout"), sourceWg, sourceChan, targetChan)
	go helpers.Copy(toSourceLogger.Session("stderr"), sourceWg, sourceChan.Stderr(), targetChan.Stderr())
	go func() {
		sourceWg.Wait()
		sourceChan.CloseWrite()
	}()

	go ProxyRequests(toTargetLogger, newChannel.ChannelType(), sourceReqs, targetChan, targetWg)
	go ProxyRequests(toSourceLogger, newChannel.ChannelType(), targetReqs, sourceChan, sourceWg)
}

func ProxyRequests(logger lager.Logger, channelType string, reqs <-chan *ssh.Request, channel ssh.Channel, wg *sync.WaitGroup) {
	logger = logger.Session("proxy-requests", lager.Data{
		"channel-type": channelType,
	})

	logger.Info("started")
	defer func() {
		logger.Info("completed")
		wg.Wait()
		channel.Close()
	}()

	for req := range reqs {
		logger.Info("request", lager.Data{
			"type":      req.Type,
			"wantReply": req.WantReply,
			"payload":   req.Payload,
		})
		success, err := channel.SendRequest(req.Type, req.WantReply, req.Payload)
		if err != nil {
			logger.Error("send-request-failed", err)
			continue
		}

		if req.WantReply {
			req.Reply(success, nil)
		}

		if req.Type == "exit-status" {
			return
		}
	}
}

func Wait(logger lager.Logger, waiters ...Waiter) {
	wg := &sync.WaitGroup{}
	for _, waiter := range waiters {
		wg.Add(1)
		go func(waiter Waiter) {
			waiter.Wait()
			wg.Done()
		}(waiter)
	}
	wg.Wait()
}

func NewClientConn(logger lager.Logger, permissions *ssh.Permissions, tlsConfig *tls.Config) (ssh.Conn, <-chan ssh.NewChannel, <-chan *ssh.Request, error) {
	if permissions == nil || permissions.CriticalOptions == nil {
		err := errors.New("Invalid permissions from authentication")
		logger.Error("permissions-and-critical-options-required", err)
		return nil, nil, nil, err
	}

	targetConfigJson := permissions.CriticalOptions["proxy-target-config"]
	logger = logger.Session("new-client-conn", lager.Data{
		"proxy-target-config": targetConfigJson,
	})

	logger.Debug("creating-client-connection")

	var targetConfig TargetConfig
	err := json.Unmarshal([]byte(permissions.CriticalOptions["proxy-target-config"]), &targetConfig)
	if err != nil {
		logger.Error("unmarshal-failed", err)
		return nil, nil, nil, err
	}

	dialer := func() (net.Conn, error) {
		tlsConfig := tlsConfigWithServerName(tlsConfig, targetConfig.ServerCertDomainSAN)
		if tlsConfig != nil {
			nConn, err := tls.Dial("tcp", targetConfig.TLSAddress, tlsConfig)
			if err == nil {
				return nConn, nil
			}

			logger.Error("tls-dial-failed", err)
		}

		nConn, err := net.Dial("tcp", targetConfig.Address)
		if err != nil {
			logger.Error("dial-failed", err)
			return nil, err
		}

		return nConn, nil
	}

	nConn, err := dialer()
	if err != nil {
		return nil, nil, nil, err
	}

	logger.Info("connected-to-backend", lager.Data{
		"backend-address": nConn.RemoteAddr().String(),
	})

	clientConfig := &ssh.ClientConfig{}

	if targetConfig.User != "" {
		clientConfig.User = targetConfig.User
	}

	if targetConfig.PrivateKey != "" {
		key, err := ssh.ParsePrivateKey([]byte(targetConfig.PrivateKey))
		if err != nil {
			logger.Error("parsing-key-failed", err)
			return nil, nil, nil, err
		}
		clientConfig.Auth = append(clientConfig.Auth, ssh.PublicKeys(key))
	}

	if targetConfig.User != "" && targetConfig.Password != "" {
		clientConfig.Auth = append(clientConfig.Auth, ssh.Password(targetConfig.Password))
	}

	if targetConfig.HostFingerprint != "" {
		clientConfig.HostKeyCallback = func(hostname string, remote net.Addr, key ssh.PublicKey) error {
			expectedFingerprint := targetConfig.HostFingerprint

			var actualFingerprint string
			switch utf8.RuneCountInString(expectedFingerprint) {
			case helpers.MD5_FINGERPRINT_LENGTH:
				actualFingerprint = helpers.MD5Fingerprint(key)
			case helpers.SHA1_FINGERPRINT_LENGTH:
				actualFingerprint = helpers.SHA1Fingerprint(key)
			}

			if expectedFingerprint != actualFingerprint {
				err := errors.New("Host fingerprint mismatch")
				logger.Error("host-key-fingerprint-mismatch", err)
				return err
			}

			return nil
		}
	} else {
		clientConfig.HostKeyCallback = ssh.InsecureIgnoreHostKey()
	}

	conn, ch, req, err := ssh.NewClientConn(nConn, targetConfig.Address, clientConfig)
	if err != nil {
		logger.Error("handshake-failed", err)
		return nil, nil, nil, err
	}

	return conn, ch, req, nil
}

func tlsConfigWithServerName(original *tls.Config, serverName string) *tls.Config {
	if original == nil {
		return nil
	}
	new := original.Clone()
	new.ServerName = serverName
	return new
}
<commit_msg>Avoid logging private key in proxy dialer<commit_after>package proxy

import (
	"crypto/tls"
	"encoding/json"
	"errors"
	"fmt"
	"net"
	"strconv"
	"sync"
	"unicode/utf8"

	loggingclient "code.cloudfoundry.org/diego-logging-client"
	"code.cloudfoundry.org/diego-ssh/helpers"
	"code.cloudfoundry.org/lager"
	"golang.org/x/crypto/ssh"
)

const (
	sshConnectionsMetric = "ssh-connections"
)

type Waiter interface {
	Wait() error
}

type TargetConfig struct {
	Address             string `json:"address"`
	TLSAddress          string `json:"tls_address"`
	ServerCertDomainSAN string `json:"server_cert_domain_san"`
	HostFingerprint     string `json:"host_fingerprint"`
	User                string `json:"user,omitempty"`
	Password            string `json:"password,omitempty"`
	PrivateKey          string `json:"private_key,omitempty"`
}

type LogMessage struct {
	Guid    string `json:"guid"`
	Message string `json:"message"`
	Index   int    `json:"index"`
}

type Proxy struct {
	logger       lager.Logger
	serverConfig *ssh.ServerConfig

	connectionLock *sync.Mutex
	connections    int
	metronClient   loggingclient.IngressClient

	tlsConfig *tls.Config
}

func New(
	logger lager.Logger,
	serverConfig *ssh.ServerConfig,
	metronClient loggingclient.IngressClient,
	tlsConfig *tls.Config,
) *Proxy {
	return &Proxy{
		logger:         logger,
		serverConfig:   serverConfig,
		connectionLock: &sync.Mutex{},
		metronClient:   metronClient,
		tlsConfig:      tlsConfig,
	}
}

func (p *Proxy) HandleConnection(netConn net.Conn) {
	logger := p.logger.Session("handle-connection")
	defer netConn.Close()

	serverConn, serverChannels, serverRequests, err := ssh.NewServerConn(netConn, p.serverConfig)
	if err != nil {
		return
	}
	defer serverConn.Close()

	clientConn, clientChannels, clientRequests, err := NewClientConn(logger, serverConn.Permissions, p.tlsConfig)
	if err != nil {
		return
	}

	logMessage := extractLogMessage(logger, serverConn.Permissions)

	defer func() {
		if logMessage != nil {
			endMessage := fmt.Sprintf("Remote access ended for %s", serverConn.RemoteAddr().String())
			p.metronClient.SendAppLog(logMessage.Guid, endMessage, "SSH", strconv.Itoa(logMessage.Index))
		}
		clientConn.Close()
	}()

	if logMessage != nil {
		p.metronClient.SendAppLog(logMessage.Guid, logMessage.Message, "SSH", strconv.Itoa(logMessage.Index))
	}

	fromClientLogger := logger.Session("from-client")
	fromDaemonLogger := logger.Session("from-daemon")

	go ProxyGlobalRequests(fromClientLogger, clientConn, serverRequests)
	go ProxyGlobalRequests(fromDaemonLogger, serverConn, clientRequests)

	go ProxyChannels(fromClientLogger, clientConn, serverChannels)
	go ProxyChannels(fromDaemonLogger, serverConn, clientChannels)

	p.connectionLock.Lock()
	p.connections++
	err = p.metronClient.SendMetric(sshConnectionsMetric, p.connections)
	if err != nil {
		logger.Error("failed-to-send-ssh-connections-metric", err)
	}
	p.connectionLock.Unlock()

	defer func() {
		p.emitConnectionClosing(logger)
	}()

	Wait(logger, serverConn, clientConn)
}

func (p *Proxy) emitConnectionClosing(logger lager.Logger) {
	p.connectionLock.Lock()
	p.connections--
	err := p.metronClient.SendMetric(sshConnectionsMetric, p.connections)
	p.connectionLock.Unlock()

	if err != nil {
		logger.Error("failed-to-send-ssh-connections-metric", err)
	}
}

func extractLogMessage(logger lager.Logger, perms *ssh.Permissions) *LogMessage {
	logMessageJson := perms.CriticalOptions["log-message"]
	if logMessageJson == "" {
		return nil
	}

	logMessage := &LogMessage{}
	err := json.Unmarshal([]byte(logMessageJson), logMessage)
	if err != nil {
		logger.Error("json-unmarshal-failed", err)
		return nil
	}

	return logMessage
}

func ProxyGlobalRequests(logger lager.Logger, conn ssh.Conn, reqs <-chan *ssh.Request) {
	logger = logger.Session("proxy-global-requests")

	logger.Info("started")
	defer logger.Info("completed")

	for req := range reqs {
		logger.Info("request", lager.Data{
			"type":      req.Type,
			"wantReply": req.WantReply,
			"payload":   req.Payload,
		})

		success, reply, err := conn.SendRequest(req.Type, req.WantReply, req.Payload)
		if err != nil {
			logger.Error("send-request-failed", err)
			continue
		}

		if req.WantReply {
			req.Reply(success, reply)
		}
	}
}

func ProxyChannels(logger lager.Logger, conn ssh.Conn, channels <-chan ssh.NewChannel) {
	logger = logger.Session("proxy-channels")

	logger.Info("started")
	defer func() {
		logger.Info("completed")
		conn.Close()
	}()

	for newChannel := range channels {
		handleNewChannel(logger, conn, newChannel)
	}
}

func handleNewChannel(logger lager.Logger, conn ssh.Conn, newChannel ssh.NewChannel) {
	logger.Info("new-channel", lager.Data{
		"channelType": newChannel.ChannelType(),
		"extraData":   newChannel.ExtraData(),
	})

	logger.Debug("openning-channel-to-daemon")

	targetChan, targetReqs, err := conn.OpenChannel(newChannel.ChannelType(), newChannel.ExtraData())
	if err != nil {
		logger.Error("failed-to-open-channel", err)
		if openErr, ok := err.(*ssh.OpenChannelError); ok {
			newChannel.Reject(openErr.Reason, openErr.Message)
		} else {
			newChannel.Reject(ssh.ConnectionFailed, err.Error())
		}
		return
	}
	logger.Debug("opened-channel-to-daemon")

	sourceChan, sourceReqs, err := newChannel.Accept()
	if err != nil {
		targetChan.Close()
		return
	}
	logger.Debug("accepted-channel-from-client")

	toTargetLogger := logger.Session("to-target")
	toSourceLogger := logger.Session("to-source")

	targetWg := &sync.WaitGroup{}
	sourceWg := &sync.WaitGroup{}

	targetWg.Add(2)
	go helpers.Copy(toTargetLogger.Session("stdout"), targetWg, targetChan, sourceChan)
	go helpers.Copy(toTargetLogger.Session("stderr"), targetWg, targetChan.Stderr(), sourceChan.Stderr())
	go func() {
		targetWg.Wait()
		targetChan.CloseWrite()
	}()

	sourceWg.Add(2)
	go helpers.Copy(toSourceLogger.Session("stdout"), sourceWg, sourceChan, targetChan)
	go helpers.Copy(toSourceLogger.Session("stderr"), sourceWg, sourceChan.Stderr(), targetChan.Stderr())
	go func() {
		sourceWg.Wait()
		sourceChan.CloseWrite()
	}()

	go ProxyRequests(toTargetLogger, newChannel.ChannelType(), sourceReqs, targetChan, targetWg)
	go ProxyRequests(toSourceLogger, newChannel.ChannelType(), targetReqs, sourceChan, sourceWg)
}

func ProxyRequests(logger lager.Logger, channelType string, reqs <-chan *ssh.Request, channel ssh.Channel, wg *sync.WaitGroup) {
	logger = logger.Session("proxy-requests", lager.Data{
		"channel-type": channelType,
	})

	logger.Info("started")
	defer func() {
		logger.Info("completed")
		wg.Wait()
		channel.Close()
	}()

	for req := range reqs {
		logger.Info("request", lager.Data{
			"type":      req.Type,
			"wantReply": req.WantReply,
			"payload":   req.Payload,
		})
		success, err := channel.SendRequest(req.Type, req.WantReply, req.Payload)
		if err != nil {
			logger.Error("send-request-failed", err)
			continue
		}

		if req.WantReply {
			req.Reply(success, nil)
		}

		if req.Type == "exit-status" {
			return
		}
	}
}

func Wait(logger lager.Logger, waiters ...Waiter) {
	wg := &sync.WaitGroup{}
	for _, waiter := range waiters {
		wg.Add(1)
		go func(waiter Waiter) {
			waiter.Wait()
			wg.Done()
		}(waiter)
	}
	wg.Wait()
}

func NewClientConn(logger lager.Logger, permissions *ssh.Permissions, tlsConfig *tls.Config) (ssh.Conn, <-chan ssh.NewChannel, <-chan *ssh.Request, error) {
	if permissions == nil || permissions.CriticalOptions == nil {
		err := errors.New("Invalid permissions from authentication")
		logger.Error("permissions-and-critical-options-required", err)
		return nil, nil, nil, err
	}

	targetConfigJson := permissions.CriticalOptions["proxy-target-config"]
	logger = logger.Session("new-client-conn")

	logger.Debug("creating-client-connection", lager.Data{
		"proxy-target-config": targetConfigJson,
	})

	var targetConfig TargetConfig
	err := json.Unmarshal([]byte(permissions.CriticalOptions["proxy-target-config"]), &targetConfig)
	if err != nil {
		logger.Error("unmarshal-failed", err)
		return nil, nil, nil, err
	}

	dialer := func() (net.Conn, error) {
		tlsConfig := tlsConfigWithServerName(tlsConfig, targetConfig.ServerCertDomainSAN)
		if tlsConfig != nil {
			nConn, err := tls.Dial("tcp", targetConfig.TLSAddress, tlsConfig)
			if err == nil {
				return nConn, nil
			}

			logger.Error("tls-dial-failed", err, lager.Data{
				"tcp_address":            targetConfig.TLSAddress,
				"server_cert_domain_san": targetConfig.ServerCertDomainSAN,
			})
		}

		nConn, err := net.Dial("tcp", targetConfig.Address)
		if err != nil {
			logger.Error("dial-failed", err, lager.Data{
				"address": targetConfig.Address,
			})
			return nil, err
		}

		return nConn, nil
	}

	nConn, err := dialer()
	if err != nil {
		return nil, nil, nil, err
	}

	logger.Info("connected-to-backend", lager.Data{
		"backend-address": nConn.RemoteAddr().String(),
	})

	clientConfig := &ssh.ClientConfig{}

	if targetConfig.User != "" {
		clientConfig.User = targetConfig.User
	}

	if targetConfig.PrivateKey != "" {
		key, err := ssh.ParsePrivateKey([]byte(targetConfig.PrivateKey))
		if err != nil {
			logger.Error("parsing-key-failed", err)
			return nil, nil, nil, err
		}
		clientConfig.Auth = append(clientConfig.Auth, ssh.PublicKeys(key))
	}

	if targetConfig.User != "" && targetConfig.Password != "" {
		clientConfig.Auth = append(clientConfig.Auth, ssh.Password(targetConfig.Password))
	}

	if targetConfig.HostFingerprint != "" {
		clientConfig.HostKeyCallback = func(hostname string, remote net.Addr, key ssh.PublicKey) error {
			expectedFingerprint := targetConfig.HostFingerprint

			var actualFingerprint string
			switch utf8.RuneCountInString(expectedFingerprint) {
			case helpers.MD5_FINGERPRINT_LENGTH:
				actualFingerprint = helpers.MD5Fingerprint(key)
			case helpers.SHA1_FINGERPRINT_LENGTH:
				actualFingerprint = helpers.SHA1Fingerprint(key)
			}

			if expectedFingerprint != actualFingerprint {
				err := errors.New("Host fingerprint mismatch")
				logger.Error("host-key-fingerprint-mismatch", err)
				return err
			}

			return nil
		}
	} else {
		clientConfig.HostKeyCallback = ssh.InsecureIgnoreHostKey()
	}

	conn, ch, req, err := ssh.NewClientConn(nConn, targetConfig.Address, clientConfig)
	if err != nil {
		logger.Error("handshake-failed", err)
		return nil, nil, nil, err
	}

	return conn, ch, req, nil
}

func tlsConfigWithServerName(original *tls.Config, serverName string) *tls.Config {
	if original == nil {
		return nil
	}
	new := original.Clone()
	new.ServerName = serverName
	return new
}
<|endoftext|>


<commit_before>package main

import (
	"bytes"
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"math/rand"
	"net"
	"net/http"
	"net/url"
	"strings"
	"time"

	"fancyirc/types"

	"github.com/sorcix/irc"
)

var (
	servers = flag.String("servers",
		"localhost:8001",
		"(comma-separated) list of host:port network addresses of the server(s) to connect to")

	listen = flag.String("listen",
		"localhost:6667",
		"host:port to listen on for IRC connections")

	currentMaster string
	allServers    []string
)

const (
	pathCreateSession = "/fancyirc/v1/session"
	pathDeleteSession = "/fancyirc/v1/%s"
	pathPostMessage   = "/fancyirc/v1/%s/message"
	pathGetMessages   = "/fancyirc/v1/%s/messages?lastseen=%d"
)

// TODO(secure): persistent state:
// - the follow targets (channels)
// - the last known server(s) in the network. added to *servers
// - the last seen message id
// for hosted mode, this state is stored per-nickname, ideally encrypted with password

func sendFancyMessage(logPrefix, target, path string, data []byte) (*http.Response, error) {
	resp, err := http.Post(
		fmt.Sprintf("http://%s%s", target, path),
		"application/json",
		bytes.NewBuffer(data))

	if err != nil {
		// TODO(secure): try one of the other servers.
		return resp, err
	}

	if resp.StatusCode == http.StatusTemporaryRedirect {
		loc := resp.Header.Get("Location")
		if loc == "" {
			return resp, fmt.Errorf("Redirect has no Location header")
		}
		u, err := url.Parse(loc)
		if err != nil {
			return resp, fmt.Errorf("Could not parse redirection %q: %v", loc, err)
		}

		return sendFancyMessage(logPrefix, u.Host, path, data)
	}

	if resp.StatusCode != 200 {
		data, _ := ioutil.ReadAll(resp.Body)
		return resp, fmt.Errorf("sendFancyMessage(%s) failed with %v: %s", path, resp.Status, string(data))
	}

	log.Printf("%s ->fancy: %q\n", logPrefix, string(data))

	currentMaster = target
	return resp, nil
}

func sendIRCMessage(logPrefix string, ircConn *irc.Conn, msg irc.Message) {
	if err := ircConn.Encode(&msg); err != nil {
		log.Printf("%s Error sending IRC message %q: %v. Closing connection.\n", logPrefix, msg.Bytes(), err)
		// This leads to an error in .Decode(), terminating the handleIRC goroutine.
		ircConn.Close()
		return
	}
	log.Printf("%s ->irc: %q\n", logPrefix, msg.Bytes())
}

func createFancySession(logPrefix string) (session string, prefix irc.Prefix, err error) {
	var resp *http.Response
	resp, err = sendFancyMessage(logPrefix, currentMaster, pathCreateSession, []byte{})
	if err != nil {
		return
	}
	defer resp.Body.Close()

	type createSessionReply struct {
		Sessionid string
		Prefix    string
	}

	var createreply createSessionReply

	if err = json.NewDecoder(resp.Body).Decode(&createreply); err != nil {
		return
	}

	session = createreply.Sessionid
	prefix = irc.Prefix{Name: createreply.Prefix}
	return
}

func handleIRC(conn net.Conn) {
	var (
		logPrefix     = conn.RemoteAddr().String()
		ircConn       = irc.NewConn(conn)
		ircErrors     = make(chan error)
		ircMessages   = make(chan irc.Message)
		fancyMessages = make(chan string)

		ircPrefix irc.Prefix
		session   string
		quitmsg   string
		done      bool
		pingSent  bool
		err       error
	)

	session, ircPrefix, err = createFancySession(logPrefix)
	if err != nil {
		log.Printf("%s Could not create fancyirc session: %v\n", logPrefix, err)
		sendIRCMessage(logPrefix, ircConn, irc.Message{
			Command:  "ERROR",
			Trailing: fmt.Sprintf("Could not create fancyirc session: %v", err),
		})

		ircConn.Close()
		return
	}

	go func() {
		for {
			message, err := ircConn.Decode()
			if err != nil {
				ircErrors <- err
				return
			}
			log.Printf("%s <-irc: %q\n", logPrefix, message.Bytes())
			ircMessages <- *message
		}
	}()

	// TODO(secure): periodically get all the servers in the network, overwrite allServers (so that deletions work)

	go func() {
		var lastSeen types.FancyId

		for !done {
			host := allServers[rand.Intn(len(allServers))]
			// TODO(secure): exponential backoff in a per-server fashion
			log.Printf("%s Connecting to %q...\n", logPrefix, host)
			// TODO(secure): build targets (= filters) and add them to the url
			hostUrl := fmt.Sprintf("http://%s"+pathGetMessages, host, session, lastSeen)
			resp, err := http.Get(hostUrl)
			if err != nil {
				log.Printf("%s HTTP GET %q failed: %v\n", logPrefix, hostUrl, err)
				continue
			}

			if resp.StatusCode != 200 {
				log.Printf("%s Received unexpected status code from %q: %v\n", logPrefix, host, resp.Status)
				continue
			}

			// We set the host as currentMaster, not because the host is the
			// master, but because it is reachable. When sending messages, we will
			// either reach the master by chance or get redirected, at which point
			// we update currentMaster.
			currentMaster = host

			dec := json.NewDecoder(resp.Body)
			for !done {
				// TODO(secure): we need a ping message here as well, so that we can detect timeouts quickly. It could include the current servers.
				var msg types.FancyMessage
				if err := dec.Decode(&msg); err != nil {
					log.Printf("%s Protocol error on %q: Could not decode response chunk as JSON: %v\n", logPrefix, host, err)
					break
				}

				log.Printf("%s <-fancy: %q\n", logPrefix, msg.Data)
				fancyMessages <- msg.Data
				lastSeen = msg.Id
			}
			resp.Body.Close()
		}

		close(fancyMessages)

		log.Printf("Disconnecting.\n")
	}()

	// Read all remaining messages to prevent goroutine hangs.
	defer func() {
		for _ = range fancyMessages {
		}

		log.Printf("TODO: close session with msg %q\n", quitmsg)
		// TODO: close the session on the server
	}()

	for {
		select {
		case <-time.After(1 * time.Minute):
			// After no traffic in either direction for 1 minute, we send a PING
			// message. If a PING message was already sent, this means that we did
			// not receive a PONG message, so we close the connection with at
			// timeout.
			if pingSent {
				quitmsg = "ping timeout"
				ircConn.Close()
			} else {
				sendIRCMessage(logPrefix, ircConn, irc.Message{
					Prefix:  &ircPrefix,
					Command: irc.PING,
					Params:  []string{"fancyirc.proxy"},
				})
			}

		case err := <-ircErrors:
			log.Printf("Error in IRC client connection: %v\n", err)
			done = true
			return

		case msg := <-fancyMessages:
			if _, err := fmt.Fprintf(conn, "%s\n", msg); err != nil {
				log.Fatal(err)
			}

		case message := <-ircMessages:
			switch message.Command {
			case irc.PONG:
				log.Printf("%s received PONG reply.\n", logPrefix)
				pingSent = false
			case irc.PING:
				sendIRCMessage(logPrefix, ircConn, irc.Message{
					Prefix:  &ircPrefix,
					Command: irc.PONG,
					Params:  message.Params,
				})
			case irc.QUIT:
				quitmsg = message.Trailing
			default:
				//case irc.NICK:
				//	log.Printf("requested nickname is %q\n", message.Params[0])
				//	nick = message.Params[0]
				//	// TODO(secure): this needs to create a session on the IRC server.
				//	// TODO(secure): figure out whether we want to have at most 1 irc connection per nickname/session.
				//case irc.USER:
				//	// TODO(secure): the irc server, not the proxy, is supposed to send these messages
				//	// TODO(secure): send 002, 003, 004, 005, 251, 252, 254, 255, 265, 266, [motd = 375, 372, 376]
				//	reply(irc.Message{
				//		Command:  irc.RPL_WELCOME,
				//		Params:   []string{nick},
				//		Trailing: "Welcome to fancyirc :)",
				//	})
				//case irc.PRIVMSG:
				// TODO: we need to associate a session with this.
				if _, err := sendFancyMessage(logPrefix, currentMaster, fmt.Sprintf(pathPostMessage, session), message.Bytes()); err != nil {
					// TODO(secure): what should we do here?
					log.Printf("message could not be sent: %v\n", err)
				}
			}
		}
	}
}

func main() {
	flag.Parse()

	rand.Seed(time.Now().Unix())

	// Start with any server. Will be overwritten later.
	allServers = strings.Split(*servers, ",")
	if len(allServers) == 0 {
		log.Fatalf("Invalid -servers value (%q). Need at least one server.\n", *servers)
	}
	currentMaster = allServers[0]

	ln, err := net.Listen("tcp", *listen)
	if err != nil {
		log.Fatal(err)
	}

	log.Printf("fancyirc proxy listening on %q\n", *listen)

	for {
		conn, err := ln.Accept()
		if err != nil {
			log.Printf("Could not accept IRC client connection: %v\n", err)
			continue
		}
		go handleIRC(conn)
	}
}
<commit_msg>update comments<commit_after>package main

import (
	"bytes"
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"math/rand"
	"net"
	"net/http"
	"net/url"
	"strings"
	"time"

	"fancyirc/types"

	"github.com/sorcix/irc"
)

var (
	servers = flag.String("servers",
		"localhost:8001",
		"(comma-separated) list of host:port network addresses of the server(s) to connect to")

	listen = flag.String("listen",
		"localhost:6667",
		"host:port to listen on for IRC connections")

	currentMaster string
	allServers    []string
)

const (
	pathCreateSession = "/fancyirc/v1/session"
	pathDeleteSession = "/fancyirc/v1/%s"
	pathPostMessage   = "/fancyirc/v1/%s/message"
	pathGetMessages   = "/fancyirc/v1/%s/messages?lastseen=%d"
)

// TODO(secure): persistent state:
// - the last known server(s) in the network. added to *servers
// - for resuming sessions (later): the last seen message id, perhaps setup messages (JOINs, MODEs, …)
// for hosted mode, this state is stored per-nickname, ideally encrypted with password

func sendFancyMessage(logPrefix, target, path string, data []byte) (*http.Response, error) {
	resp, err := http.Post(
		fmt.Sprintf("http://%s%s", target, path),
		"application/json",
		bytes.NewBuffer(data))

	if err != nil {
		// TODO(secure): try one of the other servers.
		return resp, err
	}

	if resp.StatusCode == http.StatusTemporaryRedirect {
		loc := resp.Header.Get("Location")
		if loc == "" {
			return resp, fmt.Errorf("Redirect has no Location header")
		}
		u, err := url.Parse(loc)
		if err != nil {
			return resp, fmt.Errorf("Could not parse redirection %q: %v", loc, err)
		}

		return sendFancyMessage(logPrefix, u.Host, path, data)
	}

	if resp.StatusCode != 200 {
		data, _ := ioutil.ReadAll(resp.Body)
		return resp, fmt.Errorf("sendFancyMessage(%s) failed with %v: %s", path, resp.Status, string(data))
	}

	log.Printf("%s ->fancy: %q\n", logPrefix, string(data))

	currentMaster = target
	return resp, nil
}

func sendIRCMessage(logPrefix string, ircConn *irc.Conn, msg irc.Message) {
	if err := ircConn.Encode(&msg); err != nil {
		log.Printf("%s Error sending IRC message %q: %v. Closing connection.\n", logPrefix, msg.Bytes(), err)
		// This leads to an error in .Decode(), terminating the handleIRC goroutine.
		ircConn.Close()
		return
	}
	log.Printf("%s ->irc: %q\n", logPrefix, msg.Bytes())
}

func createFancySession(logPrefix string) (session string, prefix irc.Prefix, err error) {
	var resp *http.Response
	resp, err = sendFancyMessage(logPrefix, currentMaster, pathCreateSession, []byte{})
	if err != nil {
		return
	}
	defer resp.Body.Close()

	type createSessionReply struct {
		Sessionid string
		Prefix    string
	}

	var createreply createSessionReply

	if err = json.NewDecoder(resp.Body).Decode(&createreply); err != nil {
		return
	}

	session = createreply.Sessionid
	prefix = irc.Prefix{Name: createreply.Prefix}
	return
}

func handleIRC(conn net.Conn) {
	var (
		logPrefix     = conn.RemoteAddr().String()
		ircConn       = irc.NewConn(conn)
		ircErrors     = make(chan error)
		ircMessages   = make(chan irc.Message)
		fancyMessages = make(chan string)

		ircPrefix irc.Prefix
		session   string
		quitmsg   string
		done      bool
		pingSent  bool
		err       error
	)

	session, ircPrefix, err = createFancySession(logPrefix)
	if err != nil {
		log.Printf("%s Could not create fancyirc session: %v\n", logPrefix, err)
		sendIRCMessage(logPrefix, ircConn, irc.Message{
			Command:  "ERROR",
			Trailing: fmt.Sprintf("Could not create fancyirc session: %v", err),
		})

		ircConn.Close()
		return
	}

	go func() {
		for {
			message, err := ircConn.Decode()
			if err != nil {
				ircErrors <- err
				return
			}
			log.Printf("%s <-irc: %q\n", logPrefix, message.Bytes())
			ircMessages <- *message
		}
	}()

	// TODO(secure): periodically get all the servers in the network, overwrite allServers (so that deletions work)

	go func() {
		var lastSeen types.FancyId

		for !done {
			host := allServers[rand.Intn(len(allServers))]
			// TODO(secure): exponential backoff in a per-server fashion
			log.Printf("%s Connecting to %q...\n", logPrefix, host)
			hostUrl := fmt.Sprintf("http://%s"+pathGetMessages, host, session, lastSeen)
			resp, err := http.Get(hostUrl)
			if err != nil {
				log.Printf("%s HTTP GET %q failed: %v\n", logPrefix, hostUrl, err)
				continue
			}

			if resp.StatusCode != 200 {
				log.Printf("%s Received unexpected status code from %q: %v\n", logPrefix, host, resp.Status)
				continue
			}

			// We set the host as currentMaster, not because the host is the
			// master, but because it is reachable. When sending messages, we will
			// either reach the master by chance or get redirected, at which point
			// we update currentMaster.
			currentMaster = host

			dec := json.NewDecoder(resp.Body)
			for !done {
				// TODO(secure): we need a ping message here as well, so that we can detect timeouts quickly. It could include the current servers.
				var msg types.FancyMessage
				if err := dec.Decode(&msg); err != nil {
					log.Printf("%s Protocol error on %q: Could not decode response chunk as JSON: %v\n", logPrefix, host, err)
					break
				}

				log.Printf("%s <-fancy: %q\n", logPrefix, msg.Data)
				fancyMessages <- msg.Data
				lastSeen = msg.Id
			}
			resp.Body.Close()
		}

		close(fancyMessages)

		log.Printf("Disconnecting.\n")
	}()

	// Read all remaining messages to prevent goroutine hangs.
	defer func() {
		for _ = range fancyMessages {
		}

		log.Printf("TODO: close session with msg %q\n", quitmsg)
		// TODO: close the session on the server
	}()

	for {
		select {
		case <-time.After(1 * time.Minute):
			// After no traffic in either direction for 1 minute, we send a PING
			// message. If a PING message was already sent, this means that we did
			// not receive a PONG message, so we close the connection with at
			// timeout.
			if pingSent {
				quitmsg = "ping timeout"
				ircConn.Close()
			} else {
				sendIRCMessage(logPrefix, ircConn, irc.Message{
					Prefix:  &ircPrefix,
					Command: irc.PING,
					Params:  []string{"fancyirc.proxy"},
				})
			}

		case err := <-ircErrors:
			log.Printf("Error in IRC client connection: %v\n", err)
			done = true
			return

		case msg := <-fancyMessages:
			if _, err := fmt.Fprintf(conn, "%s\n", msg); err != nil {
				log.Fatal(err)
			}

		case message := <-ircMessages:
			switch message.Command {
			case irc.PONG:
				log.Printf("%s received PONG reply.\n", logPrefix)
				pingSent = false
			case irc.PING:
				sendIRCMessage(logPrefix, ircConn, irc.Message{
					Prefix:  &ircPrefix,
					Command: irc.PONG,
					Params:  message.Params,
				})
			case irc.QUIT:
				quitmsg = message.Trailing
			default:
				if _, err := sendFancyMessage(logPrefix, currentMaster, fmt.Sprintf(pathPostMessage, session), message.Bytes()); err != nil {
					// TODO(secure): what should we do here?
					log.Printf("message could not be sent: %v\n", err)
				}
			}
		}
	}
}

func main() {
	flag.Parse()

	rand.Seed(time.Now().Unix())

	// Start with any server. Will be overwritten later.
	allServers = strings.Split(*servers, ",")
	if len(allServers) == 0 {
		log.Fatalf("Invalid -servers value (%q). Need at least one server.\n", *servers)
	}
	currentMaster = allServers[0]

	ln, err := net.Listen("tcp", *listen)
	if err != nil {
		log.Fatal(err)
	}

	log.Printf("fancyirc proxy listening on %q\n", *listen)

	for {
		conn, err := ln.Accept()
		if err != nil {
			log.Printf("Could not accept IRC client connection: %v\n", err)
			continue
		}
		go handleIRC(conn)
	}
}
<|endoftext|>


<commit_before>// proxy bridges between IRC clients (RFC1459) and fancyirc servers.
//
// Proxy instances are supposed to be long-running, and ideally as close to the
// IRC client as possible, e.g. on the same machine. When running on the same
// machine, there should not be any network problems between the IRC client and
// the proxy. Network problems between the proxy and a fancyirc network are
// handled transparently.
package main

import (
	"bytes"
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"math"
	"math/rand"
	"net"
	"net/http"
	"net/url"
	"strings"
	"sync"
	"time"

	"fancyirc/types"

	"github.com/sorcix/irc"
)

var (
	network = flag.String("network",
		"",
		`DNS name to connect to (e.g. "robustirc.net"). The _robustirc._tcp SRV record must be present.`)

	serversList = flag.String("servers",
		"",
		"(comma-separated) list of host:port network addresses of the server(s) to connect to")

	listen = flag.String("listen",
		"localhost:6667",
		"host:port to listen on for IRC connections")

	serversMu     sync.RWMutex
	currentMaster string
	allServers    []string
)

const (
	pathCreateSession = "/robustirc/v1/session"
	pathDeleteSession = "/robustirc/v1/%s"
	pathPostMessage   = "/robustirc/v1/%s/message"
	pathGetMessages   = "/robustirc/v1/%s/messages?lastseen=%s"
)

// TODO(secure): persistent state:
// - the last known server(s) in the network. added to *servers
// - for resuming sessions (later): the last seen message id, perhaps setup messages (JOINs, MODEs, …)
// for hosted mode, this state is stored per-nickname, ideally encrypted with password

// servers returns all configured servers, with the last-known master prepended.
func servers() []string {
	serversMu.RLock()
	defer serversMu.RUnlock()
	return append([]string{currentMaster}, allServers...)
}

func sendFancyMessage(logPrefix, sessionauth, method string, targets []string, path string, data []byte) (*http.Response, error) {
	var (
		resp   *http.Response
		target string
	)
	for {
		var soonest time.Duration
		target = ""
		for target == "" {
			target, soonest = nextCandidate(targets)
			if target == "" {
				log.Printf("%s Waiting %v for back-off time to expire…\n", logPrefix, soonest)
				time.Sleep(soonest)
			}
		}

		log.Printf("%s targets = %v, candidate = %s\n", logPrefix, targets, target)

		var err error
		req, err := http.NewRequest(method, fmt.Sprintf("https://%s%s", target, path), bytes.NewBuffer(data))
		if err != nil {
			return nil, err
		}
		req.Header.Set("X-Session-Auth", sessionauth)
		req.Header.Set("Content-Type", "application/json")
		resp, err = http.DefaultClient.Do(req)

		if err != nil {
			log.Printf("%s %v\n", logPrefix, err)
			serverFailed(target)
			continue
		}

		if resp.StatusCode == http.StatusTemporaryRedirect {
			loc := resp.Header.Get("Location")
			if loc == "" {
				return nil, fmt.Errorf("Redirect has no Location header")
			}
			u, err := url.Parse(loc)
			if err != nil {
				return nil, fmt.Errorf("Could not parse redirection %q: %v", loc, err)
			}

			resp.Body.Close()

			log.Printf("%s %q redirects us to %q\n", logPrefix, target, u.Host)

			// Even though the server did not actually fail, it did not answer our
			// request either. To prevent hammering it, mark it as failed for
			// back-off purposes.
			serverFailed(target)
			targets = append([]string{u.Host}, targets...)
			continue
		}

		if resp.StatusCode != 200 {
			data, _ := ioutil.ReadAll(resp.Body)
			resp.Body.Close()
			log.Printf("%s sendFancyMessage(%q) failed with %v: %s", logPrefix, path, resp.Status, string(data))
			serverFailed(target)
			continue
		}

		break
	}
	log.Printf("%s ->fancy: %q\n", logPrefix, string(data))

	serversMu.Lock()
	currentMaster = target
	serversMu.Unlock()
	return resp, nil
}

func sendIRCMessage(logPrefix string, ircConn *irc.Conn, msg irc.Message) {
	if err := ircConn.Encode(&msg); err != nil {
		log.Printf("%s Error sending IRC message %q: %v. Closing connection.\n", logPrefix, msg.Bytes(), err)
		// This leads to an error in .Decode(), terminating the handleIRC goroutine.
		ircConn.Close()
		return
	}
	log.Printf("%s ->irc: %q\n", logPrefix, msg.Bytes())
}

func createFancySession(logPrefix string) (session string, sessionauth string, prefix irc.Prefix, err error) {
	var resp *http.Response
	resp, err = sendFancyMessage(logPrefix, "", "POST", servers(), pathCreateSession, []byte{})
	if err != nil {
		return
	}
	defer resp.Body.Close()

	type createSessionReply struct {
		Sessionid   string
		Sessionauth string
		Prefix      string
	}

	var createreply createSessionReply

	if err = json.NewDecoder(resp.Body).Decode(&createreply); err != nil {
		return
	}

	session = createreply.Sessionid
	sessionauth = createreply.Sessionauth
	prefix = irc.Prefix{Name: createreply.Prefix}
	return
}

func deleteFancySession(logPrefix, sessionauth, session string, quitmsg string) error {
	type deleteSessionRequest struct {
		Quitmessage string
	}
	b, err := json.Marshal(deleteSessionRequest{Quitmessage: quitmsg})
	if err != nil {
		return err
	}
	resp, err := sendFancyMessage(logPrefix, sessionauth, "DELETE", servers(), fmt.Sprintf(pathDeleteSession, session), b)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		return fmt.Errorf("got %v, expected 200", resp.Status)
	}

	log.Printf("%s deleted session\n", logPrefix)

	return nil
}

func handleIRC(conn net.Conn) {
	var (
		logPrefix       = conn.RemoteAddr().String()
		ircConn         = irc.NewConn(conn)
		ircErrors       = make(chan error)
		ircMessages     = make(chan irc.Message)
		fancyMessages   = make(chan string)
		stopGetMessages = make(chan bool)

		ircPrefix   irc.Prefix
		session     string
		sessionauth string
		quitmsg     string
		done        bool
		pingSent    bool
		err         error
	)

	session, sessionauth, ircPrefix, err = createFancySession(logPrefix)
	if err != nil {
		log.Printf("%s Could not create RobustIRC session: %v\n", logPrefix, err)
		sendIRCMessage(logPrefix, ircConn, irc.Message{
			Command:  "ERROR",
			Trailing: fmt.Sprintf("Could not create RobustIRC session: %v", err),
		})

		ircConn.Close()
		return
	}

	go func() {
		for {
			message, err := ircConn.Decode()
			if err != nil {
				ircErrors <- err
				return
			}
			log.Printf("%s <-irc: %q\n", logPrefix, message.Bytes())
			ircMessages <- *message
		}
	}()

	go func() {
		var lastSeen types.FancyId

		for !done {
			host, resp := getMessages(logPrefix, sessionauth, session, lastSeen)

			// We set the host as currentMaster, not because the host is the
			// master, but because it is reachable. When sending messages, we will
			// either reach the master by chance or get redirected, at which point
			// we update currentMaster.
			serversMu.Lock()
			currentMaster = host
			serversMu.Unlock()

			dec := json.NewDecoder(resp.Body)
			msgchan := make(chan types.FancyMessage)
			errchan := make(chan error)

			go func() {
				for {
					var msg types.FancyMessage
					if err := dec.Decode(&msg); err != nil {
						errchan <- err
						return
					}
					msgchan <- msg
				}
			}()

		Readloop:
			for !done {
				select {
				case err := <-errchan:
					log.Printf("%s Protocol error on %q: Could not decode response chunk as JSON: %v\n", logPrefix, host, err)
					serverFailed(host)
					break Readloop

				case <-time.After(1 * time.Minute):
					log.Printf("%s Timeout (60s) on GetMessages, reconnecting…\n", logPrefix)
					serverFailed(host)
					break Readloop

				case <-stopGetMessages:
					log.Printf("%s GetMessages aborted.\n", logPrefix)
					break Readloop

				case msg := <-msgchan:
					if msg.Type == types.FancyPing {
						serversMu.Lock()
						allServers = msg.Servers
						currentMaster = msg.Currentmaster
						serversMu.Unlock()
						log.Printf("received ping (%+v). Servers are now %v\n", msg, servers())
					} else if msg.Type == types.FancyIRCToClient {
						log.Printf("%s <-fancy: %q\n", logPrefix, msg.Data)
						fancyMessages <- msg.Data
						lastSeen = msg.Id
					}
				}
			}
			resp.Body.Close()
		}

		close(fancyMessages)
	}()

	// Cancel the GetMessages goroutine, read all remaining messages to prevent
	// goroutine hangs, then delete the session.
	defer func() {
		stopGetMessages <- true
		for _ = range fancyMessages {
		}

		if err := deleteFancySession(logPrefix, sessionauth, session, quitmsg); err != nil {
			log.Printf("%s Could not delete session: %v\n", logPrefix, err)
		}
	}()

	for {
		select {
		case <-time.After(1 * time.Minute):
			// After no traffic in either direction for 1 minute, we send a PING
			// message. If a PING message was already sent, this means that we did
			// not receive a PONG message, so we close the connection with at
			// timeout.
			if pingSent {
				quitmsg = "ping timeout"
				ircConn.Close()
			} else {
				sendIRCMessage(logPrefix, ircConn, irc.Message{
					Prefix:  &ircPrefix,
					Command: irc.PING,
					Params:  []string{"robustirc.proxy"},
				})
			}

		case err := <-ircErrors:
			log.Printf("Error in IRC client connection: %v\n", err)
			done = true
			return

		case msg := <-fancyMessages:
			if _, err := fmt.Fprintf(conn, "%s\n", msg); err != nil {
				log.Fatal(err)
			}

		case message := <-ircMessages:
			switch message.Command {
			case irc.PONG:
				log.Printf("%s received PONG reply.\n", logPrefix)
				pingSent = false
			case irc.PING:
				sendIRCMessage(logPrefix, ircConn, irc.Message{
					Prefix:  &ircPrefix,
					Command: irc.PONG,
					Params:  message.Params,
				})
			case irc.QUIT:
				quitmsg = message.Trailing
				ircConn.Close()
			default:
				type postMessageRequest struct {
					Data string
				}

				b, err := json.Marshal(postMessageRequest{Data: string(message.Bytes())})
				if err != nil {
					log.Printf("Message could not be encoded as JSON: %v\n", err)
					sendIRCMessage(logPrefix, ircConn, irc.Message{
						Prefix:   &ircPrefix,
						Command:  irc.ERROR,
						Trailing: fmt.Sprintf("Message could not be encoded as JSON: %v", err),
					})
					ircConn.Close()
					break
				}

				resp, err := sendFancyMessage(logPrefix, sessionauth, "POST", servers(), fmt.Sprintf(pathPostMessage, session), b)
				if err != nil {
					// TODO(secure): what should we do here?
					log.Printf("message could not be sent: %v\n", err)
				}
				resp.Body.Close()
			}
		}
	}
}

func main() {
	flag.Parse()

	rand.Seed(time.Now().Unix())

	if *network == "" && *serversList == "" {
		log.Fatal("You must specify either -network or -servers.")
	}

	if *network != "" {
		// Try to resolve the DNS name up to 5 times. This is to be nice to
		// people in environments with flaky network connections at boot, who,
		// for some reason, don’t run this program under systemd with
		// Restart=on-failure.
		try := 0
		for {
			_, addrs, err := net.LookupSRV("robustirc", "tcp", *network)
			if err != nil {
				log.Println(err)
				if try < 4 {
					time.Sleep(time.Duration(int64(math.Pow(2, float64(try)))) * time.Second)
				} else {
					log.Fatalf("DNS lookup failed 5 times, exiting\n")
				}
				try++
				continue
			}
			for _, addr := range addrs {
				target := addr.Target
				if target[len(target)-1] == '.' {
					target = target[:len(target)-1]
				}
				allServers = append(allServers, fmt.Sprintf("%s:%d", target, addr.Port))
			}
			break
		}
	}

	if *serversList != "" {
		// Start with any server. Will be overwritten later.
		allServers = append(allServers, strings.Split(*serversList, ",")...)
		if len(allServers) == 0 {
			log.Fatalf("Invalid -servers value (%q). Need at least one server.\n", *serversList)
		}
	}

	currentMaster = allServers[0]

	ln, err := net.Listen("tcp", *listen)
	if err != nil {
		log.Fatal(err)
	}

	log.Printf("RobustIRC IRC bridge listening on %q\n", *listen)

	for {
		conn, err := ln.Accept()
		if err != nil {
			log.Printf("Could not accept IRC client connection: %v\n", err)
			continue
		}
		go handleIRC(conn)
	}
}
<commit_msg>proxy: fix keepalive. huge latency improvements<commit_after>// proxy bridges between IRC clients (RFC1459) and fancyirc servers.
//
// Proxy instances are supposed to be long-running, and ideally as close to the
// IRC client as possible, e.g. on the same machine. When running on the same
// machine, there should not be any network problems between the IRC client and
// the proxy. Network problems between the proxy and a fancyirc network are
// handled transparently.
package main

import (
	"bytes"
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"math"
	"math/rand"
	"net"
	"net/http"
	"net/url"
	"strings"
	"sync"
	"time"

	"fancyirc/types"

	"github.com/sorcix/irc"
)

var (
	network = flag.String("network",
		"",
		`DNS name to connect to (e.g. "robustirc.net"). The _robustirc._tcp SRV record must be present.`)

	serversList = flag.String("servers",
		"",
		"(comma-separated) list of host:port network addresses of the server(s) to connect to")

	listen = flag.String("listen",
		"localhost:6667",
		"host:port to listen on for IRC connections")

	serversMu     sync.RWMutex
	currentMaster string
	allServers    []string
)

const (
	pathCreateSession = "/robustirc/v1/session"
	pathDeleteSession = "/robustirc/v1/%s"
	pathPostMessage   = "/robustirc/v1/%s/message"
	pathGetMessages   = "/robustirc/v1/%s/messages?lastseen=%s"
)

// TODO(secure): persistent state:
// - the last known server(s) in the network. added to *servers
// - for resuming sessions (later): the last seen message id, perhaps setup messages (JOINs, MODEs, …)
// for hosted mode, this state is stored per-nickname, ideally encrypted with password

// servers returns all configured servers, with the last-known master prepended.
func servers() []string {
	serversMu.RLock()
	defer serversMu.RUnlock()
	return append([]string{currentMaster}, allServers...)
}

func sendFancyMessage(logPrefix, sessionauth, method string, targets []string, path string, data []byte) (*http.Response, error) {
	var (
		resp   *http.Response
		target string
	)
	for {
		var soonest time.Duration
		target = ""
		for target == "" {
			target, soonest = nextCandidate(targets)
			if target == "" {
				log.Printf("%s Waiting %v for back-off time to expire…\n", logPrefix, soonest)
				time.Sleep(soonest)
			}
		}

		log.Printf("%s targets = %v, candidate = %s\n", logPrefix, targets, target)

		var err error
		req, err := http.NewRequest(method, fmt.Sprintf("https://%s%s", target, path), bytes.NewBuffer(data))
		if err != nil {
			return nil, err
		}
		req.Header.Set("X-Session-Auth", sessionauth)
		req.Header.Set("Content-Type", "application/json")
		resp, err = http.DefaultClient.Do(req)

		if err != nil {
			log.Printf("%s %v\n", logPrefix, err)
			serverFailed(target)
			continue
		}

		if resp.StatusCode == http.StatusTemporaryRedirect {
			loc := resp.Header.Get("Location")
			if loc == "" {
				return nil, fmt.Errorf("Redirect has no Location header")
			}
			u, err := url.Parse(loc)
			if err != nil {
				return nil, fmt.Errorf("Could not parse redirection %q: %v", loc, err)
			}

			resp.Body.Close()

			log.Printf("%s %q redirects us to %q\n", logPrefix, target, u.Host)

			// Even though the server did not actually fail, it did not answer our
			// request either. To prevent hammering it, mark it as failed for
			// back-off purposes.
			serverFailed(target)
			targets = append([]string{u.Host}, targets...)
			continue
		}

		if resp.StatusCode != 200 {
			data, _ := ioutil.ReadAll(resp.Body)
			resp.Body.Close()
			log.Printf("%s sendFancyMessage(%q) failed with %v: %s", logPrefix, path, resp.Status, string(data))
			serverFailed(target)
			continue
		}

		break
	}
	log.Printf("%s ->fancy: %q\n", logPrefix, string(data))

	serversMu.Lock()
	currentMaster = target
	serversMu.Unlock()
	return resp, nil
}

func sendIRCMessage(logPrefix string, ircConn *irc.Conn, msg irc.Message) {
	if err := ircConn.Encode(&msg); err != nil {
		log.Printf("%s Error sending IRC message %q: %v. Closing connection.\n", logPrefix, msg.Bytes(), err)
		// This leads to an error in .Decode(), terminating the handleIRC goroutine.
		ircConn.Close()
		return
	}
	log.Printf("%s ->irc: %q\n", logPrefix, msg.Bytes())
}

func createFancySession(logPrefix string) (session string, sessionauth string, prefix irc.Prefix, err error) {
	var resp *http.Response
	resp, err = sendFancyMessage(logPrefix, "", "POST", servers(), pathCreateSession, []byte{})
	if err != nil {
		return
	}
	defer resp.Body.Close()
	// We need to read the entire body, otherwise net/http will not re-use this
	// connection.
	defer ioutil.ReadAll(resp.Body)

	type createSessionReply struct {
		Sessionid   string
		Sessionauth string
		Prefix      string
	}

	var createreply createSessionReply

	if err = json.NewDecoder(resp.Body).Decode(&createreply); err != nil {
		return
	}

	session = createreply.Sessionid
	sessionauth = createreply.Sessionauth
	prefix = irc.Prefix{Name: createreply.Prefix}
	return
}

func deleteFancySession(logPrefix, sessionauth, session string, quitmsg string) error {
	type deleteSessionRequest struct {
		Quitmessage string
	}
	b, err := json.Marshal(deleteSessionRequest{Quitmessage: quitmsg})
	if err != nil {
		return err
	}
	resp, err := sendFancyMessage(logPrefix, sessionauth, "DELETE", servers(), fmt.Sprintf(pathDeleteSession, session), b)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	// We need to read the entire body, otherwise net/http will not re-use this
	// connection.
	defer ioutil.ReadAll(resp.Body)

	if resp.StatusCode != 200 {
		return fmt.Errorf("got %v, expected 200", resp.Status)
	}

	log.Printf("%s deleted session\n", logPrefix)

	return nil
}

func handleIRC(conn net.Conn) {
	var (
		logPrefix       = conn.RemoteAddr().String()
		ircConn         = irc.NewConn(conn)
		ircErrors       = make(chan error)
		ircMessages     = make(chan irc.Message)
		fancyMessages   = make(chan string)
		stopGetMessages = make(chan bool)

		ircPrefix   irc.Prefix
		session     string
		sessionauth string
		quitmsg     string
		done        bool
		pingSent    bool
		err         error
	)

	session, sessionauth, ircPrefix, err = createFancySession(logPrefix)
	if err != nil {
		log.Printf("%s Could not create RobustIRC session: %v\n", logPrefix, err)
		sendIRCMessage(logPrefix, ircConn, irc.Message{
			Command:  "ERROR",
			Trailing: fmt.Sprintf("Could not create RobustIRC session: %v", err),
		})

		ircConn.Close()
		return
	}

	go func() {
		for {
			message, err := ircConn.Decode()
			if err != nil {
				ircErrors <- err
				return
			}
			log.Printf("%s <-irc: %q\n", logPrefix, message.Bytes())
			ircMessages <- *message
		}
	}()

	go func() {
		var lastSeen types.FancyId

		for !done {
			host, resp := getMessages(logPrefix, sessionauth, session, lastSeen)

			// We set the host as currentMaster, not because the host is the
			// master, but because it is reachable. When sending messages, we will
			// either reach the master by chance or get redirected, at which point
			// we update currentMaster.
			serversMu.Lock()
			currentMaster = host
			serversMu.Unlock()

			dec := json.NewDecoder(resp.Body)
			msgchan := make(chan types.FancyMessage)
			errchan := make(chan error)

			go func() {
				for {
					var msg types.FancyMessage
					if err := dec.Decode(&msg); err != nil {
						errchan <- err
						return
					}
					msgchan <- msg
				}
			}()

		Readloop:
			for !done {
				select {
				case err := <-errchan:
					log.Printf("%s Protocol error on %q: Could not decode response chunk as JSON: %v\n", logPrefix, host, err)
					serverFailed(host)
					break Readloop

				case <-time.After(1 * time.Minute):
					log.Printf("%s Timeout (60s) on GetMessages, reconnecting…\n", logPrefix)
					serverFailed(host)
					break Readloop

				case <-stopGetMessages:
					log.Printf("%s GetMessages aborted.\n", logPrefix)
					break Readloop

				case msg := <-msgchan:
					if msg.Type == types.FancyPing {
						serversMu.Lock()
						allServers = msg.Servers
						currentMaster = msg.Currentmaster
						serversMu.Unlock()
						log.Printf("received ping (%+v). Servers are now %v\n", msg, servers())
					} else if msg.Type == types.FancyIRCToClient {
						log.Printf("%s <-fancy: %q\n", logPrefix, msg.Data)
						fancyMessages <- msg.Data
						lastSeen = msg.Id
					}
				}
			}
			resp.Body.Close()
		}

		close(fancyMessages)
	}()

	// Cancel the GetMessages goroutine, read all remaining messages to prevent
	// goroutine hangs, then delete the session.
	defer func() {
		stopGetMessages <- true
		for _ = range fancyMessages {
		}

		if err := deleteFancySession(logPrefix, sessionauth, session, quitmsg); err != nil {
			log.Printf("%s Could not delete session: %v\n", logPrefix, err)
		}
	}()

	for {
		select {
		case <-time.After(1 * time.Minute):
			// After no traffic in either direction for 1 minute, we send a PING
			// message. If a PING message was already sent, this means that we did
			// not receive a PONG message, so we close the connection with at
			// timeout.
			if pingSent {
				quitmsg = "ping timeout"
				ircConn.Close()
			} else {
				sendIRCMessage(logPrefix, ircConn, irc.Message{
					Prefix:  &ircPrefix,
					Command: irc.PING,
					Params:  []string{"robustirc.proxy"},
				})
			}

		case err := <-ircErrors:
			log.Printf("Error in IRC client connection: %v\n", err)
			done = true
			return

		case msg := <-fancyMessages:
			if _, err := fmt.Fprintf(conn, "%s\n", msg); err != nil {
				log.Fatal(err)
			}

		case message := <-ircMessages:
			switch message.Command {
			case irc.PONG:
				log.Printf("%s received PONG reply.\n", logPrefix)
				pingSent = false
			case irc.PING:
				sendIRCMessage(logPrefix, ircConn, irc.Message{
					Prefix:  &ircPrefix,
					Command: irc.PONG,
					Params:  message.Params,
				})
			case irc.QUIT:
				quitmsg = message.Trailing
				ircConn.Close()
			default:
				type postMessageRequest struct {
					Data string
				}

				b, err := json.Marshal(postMessageRequest{Data: string(message.Bytes())})
				if err != nil {
					log.Printf("Message could not be encoded as JSON: %v\n", err)
					sendIRCMessage(logPrefix, ircConn, irc.Message{
						Prefix:   &ircPrefix,
						Command:  irc.ERROR,
						Trailing: fmt.Sprintf("Message could not be encoded as JSON: %v", err),
					})
					ircConn.Close()
					break
				}

				resp, err := sendFancyMessage(logPrefix, sessionauth, "POST", servers(), fmt.Sprintf(pathPostMessage, session), b)
				if err != nil {
					// TODO(secure): what should we do here?
					log.Printf("message could not be sent: %v\n", err)
				}
				// We need to read the entire body, otherwise net/http will not
				// re-use this connection.
				ioutil.ReadAll(resp.Body)
				resp.Body.Close()
			}
		}
	}
}

func main() {
	flag.Parse()

	rand.Seed(time.Now().Unix())

	if *network == "" && *serversList == "" {
		log.Fatal("You must specify either -network or -servers.")
	}

	if *network != "" {
		// Try to resolve the DNS name up to 5 times. This is to be nice to
		// people in environments with flaky network connections at boot, who,
		// for some reason, don’t run this program under systemd with
		// Restart=on-failure.
		try := 0
		for {
			_, addrs, err := net.LookupSRV("robustirc", "tcp", *network)
			if err != nil {
				log.Println(err)
				if try < 4 {
					time.Sleep(time.Duration(int64(math.Pow(2, float64(try)))) * time.Second)
				} else {
					log.Fatalf("DNS lookup failed 5 times, exiting\n")
				}
				try++
				continue
			}
			for _, addr := range addrs {
				target := addr.Target
				if target[len(target)-1] == '.' {
					target = target[:len(target)-1]
				}
				allServers = append(allServers, fmt.Sprintf("%s:%d", target, addr.Port))
			}
			break
		}
	}

	if *serversList != "" {
		// Start with any server. Will be overwritten later.
		allServers = append(allServers, strings.Split(*serversList, ",")...)
		if len(allServers) == 0 {
			log.Fatalf("Invalid -servers value (%q). Need at least one server.\n", *serversList)
		}
	}

	currentMaster = allServers[0]

	ln, err := net.Listen("tcp", *listen)
	if err != nil {
		log.Fatal(err)
	}

	log.Printf("RobustIRC IRC bridge listening on %q\n", *listen)

	for {
		conn, err := ln.Accept()
		if err != nil {
			log.Printf("Could not accept IRC client connection: %v\n", err)
			continue
		}
		go handleIRC(conn)
	}
}
<|endoftext|>


<commit_before>package main

import (
	"fmt"
	"github.com/wfleming/go-punchcard/punchcard"
	"os"
)

const defaultFileName = "~/.punch/entries.log"

type appConfig struct {
	log *punchcard.Log
}

type appCommand struct {
	run       func(*appConfig, []string)
	printHelp func()
}

var commands = map[string]appCommand{}

func main() {
	setupCommands()

	config, err := makeConfig()

	if err != nil {
		fmt.Printf("Error: %s\n", err)
		os.Exit(1)
	}

	if 0 == len(os.Args[1:]) {
		commands["help"].run(config, os.Args[1:])
		return
	}

	commandName := os.Args[1]
	command, exists := commands[commandName]

	if exists {
		command.run(config, os.Args[2:])
	} else {
		commands["help"].run(config, os.Args[2:])
	}
}

func makeConfig() (*appConfig, error) {
	return nil, nil
}

// can't do it as part of decl, or there's a reference loop
func setupCommands() {
	commands["help"] = appCommand{runHelp, printMainHelp}
	commands["in"] = appCommand{runIn, printInHelp}
	commands["out"] = appCommand{runOut, printOutHelp}
	commands["summary"] = appCommand{runSummary, printSummaryHelp}
}
<commit_msg>app init: log file create/open<commit_after>package main

import (
	"fmt"
	"github.com/wfleming/go-punchcard/punchcard"
	"os"
	"os/user"
	"path/filepath"
	"strings"
)

const defaultFilename = "~/.punch/entries.log"

type appConfig struct {
	log *punchcard.Log
}

type appCommand struct {
	run       func(*appConfig, []string)
	printHelp func()
}

var commands = map[string]appCommand{}

func main() {
	setupCommands()

	config, err := makeConfig()

	if err != nil {
		fmt.Printf("Error: %s\n", err)
		os.Exit(1)
	}

	if 0 == len(os.Args[1:]) {
		commands["help"].run(config, os.Args[1:])
		return
	}

	commandName := os.Args[1]
	command, exists := commands[commandName]

	if exists {
		command.run(config, os.Args[2:])
	} else {
		commands["help"].run(config, os.Args[2:])
	}
}

func makeConfig() (*appConfig, error) {
	var config appConfig

	stat, _ := os.Stdin.Stat()
	if (stat.Mode() & os.ModeCharDevice) == 0 {
		// this indicates data is being piped, so use stdin in for log
		config.log = punchcard.NewLog(os.Stdin, os.Stdout)
	} else {
		fh, err := getLogFile(defaultFilename)
		if err != nil {
			return nil, err
		}
		config.log = punchcard.NewLog(fh, fh)
	}

	return &config, nil
}

func getLogFile(filename string) (*os.File, error) {
	filename, err := sanitizeLogFileName(filename)
	if err != nil {
		return nil, err
	}
	if _, err := os.Stat(filename); os.IsNotExist(err) {
		// file does not exist: attempt to create it.
		// first attempt to create dir if it does not exist
		if err := os.Mkdir(filepath.Dir(filename), 0755); err != nil {
			return nil, err
		}
	}

	fh, err := os.OpenFile(filename, os.O_CREATE|os.O_RDWR, 0664)
	if err != nil {
		return nil, err
	}
	defer fh.Close()

	return fh, nil
}

func sanitizeLogFileName(filename string) (string, error) {
	var err error
	// must do ~ replacement ourselves
	if filename[:2] == "~/" {
		usr, err := user.Current()
		if err != nil {
			return "", err
		}
		homedir := usr.HomeDir
		filename = strings.Replace(filename, "~", homedir, 1)
	}

	filename, err = filepath.Abs(filename)
	if err != nil {
		return "", err
	}

	return filename, nil
}

// can't do it as part of decl, or there's a reference loop
func setupCommands() {
	commands["help"] = appCommand{runHelp, printMainHelp}
	commands["in"] = appCommand{runIn, printInHelp}
	commands["out"] = appCommand{runOut, printOutHelp}
	commands["summary"] = appCommand{runSummary, printSummaryHelp}
}
<|endoftext|>