<commit_before>/*

Package tictactoe is an exceedingly simple game based on boardgame. It serves
as an example, and also helps verify that the design and implementation of
boardgame are useful for real games.

*/
package tictactoe
<commit_msg>Start roughing out the components for tictactoe. Part of #14.<commit_after>/*

Package tictactoe is an exceedingly simple game based on boardgame. It serves
as an example, and also helps verify that the design and implementation of
boardgame are useful for real games.

*/
package tictactoe

import (
	"github.com/jkomoros/boardgame"
)

const (
	X rune = 'X'
	O rune = 'O'
)

const gameName = "tictactoe"

type playerToken struct {
	Value rune
}

func (p *playerToken) Props() []string {
	return boardgame.PropertyReaderPropsImpl(p)
}

func (p *playerToken) Prop(name string) interface{} {
	return boardgame.PropertyReaderPropImpl(p, name)
}

func ticTacToeGame() *boardgame.Game {

	chest := boardgame.NewComponentChest(gameName)

	xes := &boardgame.Deck{}

	//How many tokens of each of x's and o's do we need so that no matter who
	//goes first we always have enough?
	numTokens := 5

	//TODO: use deck.AddComponentMulti when that exists
	for i := 0; i < numTokens; i++ {
		xes.AddComponent(&boardgame.Component{
			Values: &playerToken{
				Value: X,
			},
		})
	}

	chest.AddDeck("xes", xes)

	oes := &boardgame.Deck{}

	for i := 0; i < numTokens; i++ {
		oes.AddComponent(&boardgame.Component{
			Values: &playerToken{
				Value: O,
			},
		})
	}

	chest.AddDeck("oes", oes)

	game := &boardgame.Game{
		Name: gameName,
		//TODO: state
	}

	game.SetChest(chest)

	return game

}
<|endoftext|>


<commit_before>package github

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"net"
	"net/http"
	"net/url"
	"os"
	"regexp"
	"strings"
	"time"

	"github.com/github/hub/ui"
	"github.com/github/hub/utils"
)

type verboseTransport struct {
	Transport   *http.Transport
	Verbose     bool
	OverrideURL *url.URL
	Out         io.Writer
	Colorized   bool
}

func (t *verboseTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
	if t.Verbose {
		t.dumpRequest(req)
	}

	if t.OverrideURL != nil {
		port := "80"
		if s := strings.Split(req.URL.Host, ":"); len(s) > 1 {
			port = s[1]
		}

		req = cloneRequest(req)
		req.Header.Set("X-Original-Scheme", req.URL.Scheme)
		req.Header.Set("X-Original-Port", port)
		req.URL.Scheme = t.OverrideURL.Scheme
		req.URL.Host = t.OverrideURL.Host
	}

	resp, err = t.Transport.RoundTrip(req)

	if err == nil && t.Verbose {
		t.dumpResponse(resp)
	}

	return
}

func (t *verboseTransport) dumpRequest(req *http.Request) {
	info := fmt.Sprintf("> %s %s://%s%s", req.Method, req.URL.Scheme, req.Host, req.URL.RequestURI())
	t.verbosePrintln(info)
	t.dumpHeaders(req.Header, ">")
	body := t.dumpBody(req.Body)
	if body != nil {
		// reset body since it's been read
		req.Body = body
	}
}

func (t *verboseTransport) dumpResponse(resp *http.Response) {
	info := fmt.Sprintf("< HTTP %d", resp.StatusCode)
	t.verbosePrintln(info)
	t.dumpHeaders(resp.Header, "<")
	body := t.dumpBody(resp.Body)
	if body != nil {
		// reset body since it's been read
		resp.Body = body
	}
}

func (t *verboseTransport) dumpHeaders(header http.Header, indent string) {
	dumpHeaders := []string{"Authorization", "X-GitHub-OTP", "Location"}
	for _, h := range dumpHeaders {
		v := header.Get(h)
		if v != "" {
			r := regexp.MustCompile("(?i)^(basic|token) (.+)")
			if r.MatchString(v) {
				v = r.ReplaceAllString(v, "$1 [REDACTED]")
			}

			info := fmt.Sprintf("%s %s: %s", indent, h, v)
			t.verbosePrintln(info)
		}
	}
}

func (t *verboseTransport) dumpBody(body io.ReadCloser) io.ReadCloser {
	if body == nil {
		return nil
	}

	defer body.Close()
	buf := new(bytes.Buffer)
	_, err := io.Copy(buf, body)
	utils.Check(err)

	if buf.Len() > 0 {
		t.verbosePrintln(buf.String())
	}

	return ioutil.NopCloser(buf)
}

func (t *verboseTransport) verbosePrintln(msg string) {
	if t.Colorized {
		msg = fmt.Sprintf("\033[36m%s\033[0m", msg)
	}

	fmt.Fprintln(t.Out, msg)
}

func newHttpClient(testHost string, verbose bool) *http.Client {
	var testURL *url.URL
	if testHost != "" {
		testURL, _ = url.Parse(testHost)
	}
	tr := &verboseTransport{
		Transport: &http.Transport{
			Proxy: proxyFromEnvironment,
			Dial: (&net.Dialer{
				Timeout:   30 * time.Second,
				KeepAlive: 30 * time.Second,
			}).Dial,
			TLSHandshakeTimeout: 10 * time.Second,
		},
		Verbose:     verbose,
		OverrideURL: testURL,
		Out:         ui.Stderr,
		Colorized:   ui.IsTerminal(os.Stderr),
	}

	// Implement CheckRedirect callback to fix issues with net/http.
	fixupCheckRedirect := func(req *http.Request, via []*http.Request) error {
		// net/http doesn't send a Host header on redirect requests.
		// TODO: Find or file a Go bug.
		if req.Host == "" {
			req.Host = req.URL.Host
		}

		// Maintain headers after redirect.
		// https://github.com/golang/go/issues/4800
		for key, val := range via[0].Header {
			if req.Host != via[0].Host && strings.EqualFold(key, "Authorization") {
				continue
			}
			req.Header[key] = val
		}

		// remainder should match http/Client.defaultCheckRedirect()
		if len(via) >= 10 {
			return errors.New("stopped after 10 redirects")
		}

		return nil
	}

	return &http.Client{Transport: tr, CheckRedirect: fixupCheckRedirect}
}

func cloneRequest(req *http.Request) *http.Request {
	dup := new(http.Request)
	*dup = *req
	dup.URL, _ = url.Parse(req.URL.String())
	dup.Header = make(http.Header)
	for k, s := range req.Header {
		dup.Header[k] = s
	}
	return dup
}

// An implementation of http.ProxyFromEnvironment that isn't broken
func proxyFromEnvironment(req *http.Request) (*url.URL, error) {
	proxy := os.Getenv("http_proxy")
	if proxy == "" {
		proxy = os.Getenv("HTTP_PROXY")
	}
	if proxy == "" {
		return nil, nil
	}

	proxyURL, err := url.Parse(proxy)
	if err != nil || !strings.HasPrefix(proxyURL.Scheme, "http") {
		if proxyURL, err := url.Parse("http://" + proxy); err == nil {
			return proxyURL, nil
		}
	}

	if err != nil {
		return nil, fmt.Errorf("invalid proxy address %q: %v", proxy, err)
	}

	return proxyURL, nil
}

type simpleClient struct {
	httpClient  *http.Client
	rootUrl     *url.URL
	accessToken string
}

func (c *simpleClient) performRequest(method, path string, body io.Reader, configure func(*http.Request)) (*simpleResponse, error) {
	url, err := url.Parse(path)
	if err == nil {
		url = c.rootUrl.ResolveReference(url)
		return c.performRequestUrl(method, url, body, configure, 2)
	} else {
		return nil, err
	}
}

func (c *simpleClient) performRequestUrl(method string, url *url.URL, body io.Reader, configure func(*http.Request), redirectsRemaining int) (res *simpleResponse, err error) {
	req, err := http.NewRequest(method, url.String(), body)
	if err != nil {
		return
	}
	req.Header.Set("Authorization", "token "+c.accessToken)
	req.Header.Set("User-Agent", UserAgent)
	if configure != nil {
		configure(req)
	}

	var bodyBackup io.ReadWriter
	if req.Body != nil {
		bodyBackup = &bytes.Buffer{}
		req.Body = ioutil.NopCloser(io.TeeReader(req.Body, bodyBackup))
	}

	httpResponse, err := c.httpClient.Do(req)
	if err != nil {
		return
	}

	res = &simpleResponse{httpResponse}
	if res.StatusCode == 307 && redirectsRemaining > 0 {
		url, err = url.Parse(res.Header.Get("Location"))
		if err != nil || url.Host != req.URL.Host || url.Scheme != req.URL.Scheme {
			return
		}
		res, err = c.performRequestUrl(method, url, bodyBackup, configure, redirectsRemaining-1)
	}

	return
}

func (c *simpleClient) jsonRequest(method, path string, body interface{}) (*simpleResponse, error) {
	json, err := json.Marshal(body)
	if err != nil {
		return nil, err
	}
	buf := bytes.NewBuffer(json)

	return c.performRequest(method, path, buf, func(req *http.Request) {
		req.Header.Set("Content-Type", "application/json; charset=utf-8")
	})
}

func (c *simpleClient) Get(path string) (*simpleResponse, error) {
	return c.performRequest("GET", path, nil, nil)
}

func (c *simpleClient) GetFile(path string, mimeType string) (*simpleResponse, error) {
	return c.performRequest("GET", path, nil, func(req *http.Request) {
		req.Header.Set("Accept", mimeType)
	})
}

func (c *simpleClient) Delete(path string) (*simpleResponse, error) {
	return c.performRequest("DELETE", path, nil, nil)
}

func (c *simpleClient) PostJSON(path string, payload interface{}) (*simpleResponse, error) {
	return c.jsonRequest("POST", path, payload)
}

func (c *simpleClient) PatchJSON(path string, payload interface{}) (*simpleResponse, error) {
	return c.jsonRequest("PATCH", path, payload)
}

func (c *simpleClient) PostFile(path, filename string) (*simpleResponse, error) {
	stat, err := os.Stat(filename)
	if err != nil {
		return nil, err
	}

	file, err := os.Open(filename)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	return c.performRequest("POST", path, file, func(req *http.Request) {
		req.ContentLength = stat.Size()
		req.Header.Set("Content-Type", "application/octet-stream")
	})
}

type simpleResponse struct {
	*http.Response
}

type errorInfo struct {
	Message  string       `json:"message"`
	Errors   []fieldError `json:"errors"`
	Response *http.Response
}
type fieldError struct {
	Resource string `json:"resource"`
	Message  string `json:"message"`
	Code     string `json:"code"`
	Field    string `json:"field"`
}

func (e *errorInfo) Error() string {
	return e.Message
}

func (res *simpleResponse) Unmarshal(dest interface{}) (err error) {
	defer res.Body.Close()

	body, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return
	}

	return json.Unmarshal(body, dest)
}

func (res *simpleResponse) ErrorInfo() (msg *errorInfo, err error) {
	defer res.Body.Close()

	body, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return
	}

	msg = &errorInfo{}
	err = json.Unmarshal(body, msg)
	if err == nil {
		msg.Response = res.Response
	}

	return
}
<commit_msg>Preserve headers when following GET redirects<commit_after>package github

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"net"
	"net/http"
	"net/url"
	"os"
	"regexp"
	"strings"
	"time"

	"github.com/github/hub/ui"
	"github.com/github/hub/utils"
)

type verboseTransport struct {
	Transport   *http.Transport
	Verbose     bool
	OverrideURL *url.URL
	Out         io.Writer
	Colorized   bool
}

func (t *verboseTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
	if t.Verbose {
		t.dumpRequest(req)
	}

	if t.OverrideURL != nil {
		port := "80"
		if s := strings.Split(req.URL.Host, ":"); len(s) > 1 {
			port = s[1]
		}

		req = cloneRequest(req)
		req.Header.Set("X-Original-Scheme", req.URL.Scheme)
		req.Header.Set("X-Original-Port", port)
		req.Host = req.URL.Host
		req.URL.Scheme = t.OverrideURL.Scheme
		req.URL.Host = t.OverrideURL.Host
	}

	resp, err = t.Transport.RoundTrip(req)

	if err == nil && t.Verbose {
		t.dumpResponse(resp)
	}

	return
}

func (t *verboseTransport) dumpRequest(req *http.Request) {
	info := fmt.Sprintf("> %s %s://%s%s", req.Method, req.URL.Scheme, req.URL.Host, req.URL.RequestURI())
	t.verbosePrintln(info)
	t.dumpHeaders(req.Header, ">")
	body := t.dumpBody(req.Body)
	if body != nil {
		// reset body since it's been read
		req.Body = body
	}
}

func (t *verboseTransport) dumpResponse(resp *http.Response) {
	info := fmt.Sprintf("< HTTP %d", resp.StatusCode)
	t.verbosePrintln(info)
	t.dumpHeaders(resp.Header, "<")
	body := t.dumpBody(resp.Body)
	if body != nil {
		// reset body since it's been read
		resp.Body = body
	}
}

func (t *verboseTransport) dumpHeaders(header http.Header, indent string) {
	dumpHeaders := []string{"Authorization", "X-GitHub-OTP", "Location"}
	for _, h := range dumpHeaders {
		v := header.Get(h)
		if v != "" {
			r := regexp.MustCompile("(?i)^(basic|token) (.+)")
			if r.MatchString(v) {
				v = r.ReplaceAllString(v, "$1 [REDACTED]")
			}

			info := fmt.Sprintf("%s %s: %s", indent, h, v)
			t.verbosePrintln(info)
		}
	}
}

func (t *verboseTransport) dumpBody(body io.ReadCloser) io.ReadCloser {
	if body == nil {
		return nil
	}

	defer body.Close()
	buf := new(bytes.Buffer)
	_, err := io.Copy(buf, body)
	utils.Check(err)

	if buf.Len() > 0 {
		t.verbosePrintln(buf.String())
	}

	return ioutil.NopCloser(buf)
}

func (t *verboseTransport) verbosePrintln(msg string) {
	if t.Colorized {
		msg = fmt.Sprintf("\033[36m%s\033[0m", msg)
	}

	fmt.Fprintln(t.Out, msg)
}

func newHttpClient(testHost string, verbose bool) *http.Client {
	var testURL *url.URL
	if testHost != "" {
		testURL, _ = url.Parse(testHost)
	}
	tr := &verboseTransport{
		Transport: &http.Transport{
			Proxy: proxyFromEnvironment,
			Dial: (&net.Dialer{
				Timeout:   30 * time.Second,
				KeepAlive: 30 * time.Second,
			}).Dial,
			TLSHandshakeTimeout: 10 * time.Second,
		},
		Verbose:     verbose,
		OverrideURL: testURL,
		Out:         ui.Stderr,
		Colorized:   ui.IsTerminal(os.Stderr),
	}

	return &http.Client{
		Transport: tr,
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			if len(via) > 2 {
				return fmt.Errorf("too many redirects")
			} else {
				if len(via) > 0 && via[0].Host == req.URL.Host {
					for key, vals := range via[0].Header {
						if !strings.HasPrefix(key, "X-Original-") {
							req.Header[key] = vals
						}
					}
				}
				return nil
			}
		},
	}
}

func cloneRequest(req *http.Request) *http.Request {
	dup := new(http.Request)
	*dup = *req
	dup.URL, _ = url.Parse(req.URL.String())
	dup.Header = make(http.Header)
	for k, s := range req.Header {
		dup.Header[k] = s
	}
	return dup
}

// An implementation of http.ProxyFromEnvironment that isn't broken
func proxyFromEnvironment(req *http.Request) (*url.URL, error) {
	proxy := os.Getenv("http_proxy")
	if proxy == "" {
		proxy = os.Getenv("HTTP_PROXY")
	}
	if proxy == "" {
		return nil, nil
	}

	proxyURL, err := url.Parse(proxy)
	if err != nil || !strings.HasPrefix(proxyURL.Scheme, "http") {
		if proxyURL, err := url.Parse("http://" + proxy); err == nil {
			return proxyURL, nil
		}
	}

	if err != nil {
		return nil, fmt.Errorf("invalid proxy address %q: %v", proxy, err)
	}

	return proxyURL, nil
}

type simpleClient struct {
	httpClient  *http.Client
	rootUrl     *url.URL
	accessToken string
}

func (c *simpleClient) performRequest(method, path string, body io.Reader, configure func(*http.Request)) (*simpleResponse, error) {
	url, err := url.Parse(path)
	if err == nil {
		url = c.rootUrl.ResolveReference(url)
		return c.performRequestUrl(method, url, body, configure, 2)
	} else {
		return nil, err
	}
}

func (c *simpleClient) performRequestUrl(method string, url *url.URL, body io.Reader, configure func(*http.Request), redirectsRemaining int) (res *simpleResponse, err error) {
	req, err := http.NewRequest(method, url.String(), body)
	if err != nil {
		return
	}
	req.Header.Set("Authorization", "token "+c.accessToken)
	req.Header.Set("User-Agent", UserAgent)
	if configure != nil {
		configure(req)
	}

	var bodyBackup io.ReadWriter
	if req.Body != nil {
		bodyBackup = &bytes.Buffer{}
		req.Body = ioutil.NopCloser(io.TeeReader(req.Body, bodyBackup))
	}

	httpResponse, err := c.httpClient.Do(req)
	if err != nil {
		return
	}

	res = &simpleResponse{httpResponse}
	if res.StatusCode == 307 && redirectsRemaining > 0 {
		url, err = url.Parse(res.Header.Get("Location"))
		if err != nil || url.Host != req.URL.Host || url.Scheme != req.URL.Scheme {
			return
		}
		res, err = c.performRequestUrl(method, url, bodyBackup, configure, redirectsRemaining-1)
	}

	return
}

func (c *simpleClient) jsonRequest(method, path string, body interface{}) (*simpleResponse, error) {
	json, err := json.Marshal(body)
	if err != nil {
		return nil, err
	}
	buf := bytes.NewBuffer(json)

	return c.performRequest(method, path, buf, func(req *http.Request) {
		req.Header.Set("Content-Type", "application/json; charset=utf-8")
	})
}

func (c *simpleClient) Get(path string) (*simpleResponse, error) {
	return c.performRequest("GET", path, nil, nil)
}

func (c *simpleClient) GetFile(path string, mimeType string) (*simpleResponse, error) {
	return c.performRequest("GET", path, nil, func(req *http.Request) {
		req.Header.Set("Accept", mimeType)
	})
}

func (c *simpleClient) Delete(path string) (*simpleResponse, error) {
	return c.performRequest("DELETE", path, nil, nil)
}

func (c *simpleClient) PostJSON(path string, payload interface{}) (*simpleResponse, error) {
	return c.jsonRequest("POST", path, payload)
}

func (c *simpleClient) PatchJSON(path string, payload interface{}) (*simpleResponse, error) {
	return c.jsonRequest("PATCH", path, payload)
}

func (c *simpleClient) PostFile(path, filename string) (*simpleResponse, error) {
	stat, err := os.Stat(filename)
	if err != nil {
		return nil, err
	}

	file, err := os.Open(filename)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	return c.performRequest("POST", path, file, func(req *http.Request) {
		req.ContentLength = stat.Size()
		req.Header.Set("Content-Type", "application/octet-stream")
	})
}

type simpleResponse struct {
	*http.Response
}

type errorInfo struct {
	Message  string       `json:"message"`
	Errors   []fieldError `json:"errors"`
	Response *http.Response
}
type fieldError struct {
	Resource string `json:"resource"`
	Message  string `json:"message"`
	Code     string `json:"code"`
	Field    string `json:"field"`
}

func (e *errorInfo) Error() string {
	return e.Message
}

func (res *simpleResponse) Unmarshal(dest interface{}) (err error) {
	defer res.Body.Close()

	body, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return
	}

	return json.Unmarshal(body, dest)
}

func (res *simpleResponse) ErrorInfo() (msg *errorInfo, err error) {
	defer res.Body.Close()

	body, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return
	}

	msg = &errorInfo{}
	err = json.Unmarshal(body, msg)
	if err == nil {
		msg.Response = res.Response
	}

	return
}
<|endoftext|>


<commit_before>package gli

import (
	"errors"
	"fmt"

	"github.com/PieterD/glimmer/internal/convc"
	"github.com/go-gl/gl/v2.1/gl"
)

type Program struct {
	id       uint32
	vertId   uint32
	fragId   uint32
	uniforms map[string]Uniform
}

func (program *Program) Id() uint32 {
	return program.id
}

func (program *Program) Use() {
	gl.UseProgram(program.id)
}

func (program *Program) Delete() {
	gl.DeleteProgram(program.id)
	gl.DeleteShader(program.vertId)
	gl.DeleteShader(program.fragId)
}

type programOption struct {
	arbGeometryShader4 struct {
		use bool
		source string
		inType GeometryInputType
		outType GeometryOutputType
		numOutputVertices int
	}
}

type GeometryInputType uint32

const (
	GEOM_IN_POINTS GeometryInputType = gl.POINTS
	GEOM_IN_LINES GeometryInputType = gl.LINES
	GEOM_IN_LINES_ADJACENCY GeometryInputType = gl.LINES_ADJACENCY_ARB
	GEOM_IN_TRIANGLES GeometryInputType = gl.TRIANGLES
	GEOM_IN_TRIANGLES_ADJACENCY GeometryInputType = gl.TRIANGLES_ADJACENCY_ARB
)

type GeometryOutputType uint32

const (
	GEOM_OUT_POINTS GeometryOutputType = gl.POINTS
	GEOM_OUT_LINE_STRIP GeometryOutputType = gl.LINE_STRIP
	GEOM_OUT_TRIANGLE_STRIP GeometryOutputType = gl.TRIANGLE_STRIP
)

type ProgramOption func(opt *programOption)

func ProgramArbGeometryShader4(source string, inType GeometryInputType, outType GeometryOutputType, numOutputVertices int) ProgramOption {
	return func(opt *programOption) {
		opt.arbGeometryShader4.use = true
		opt.arbGeometryShader4.source = source
		opt.arbGeometryShader4.inType = inType
		opt.arbGeometryShader4.outType = outType
		opt.arbGeometryShader4.numOutputVertices = numOutputVertices
	}
}

func NewProgram(vertexSource, fragmentSource string, opts ...ProgramOption) (_ *Program, rerr error) {
	opt := programOption{}
	for _, o := range opts {
		o(&opt)
	}
	vertexId, err := newShader(vertexSource, gl.VERTEX_SHADER)
	if err != nil {
		return nil, err
	}
	fragmentId, err := newShader(fragmentSource, gl.FRAGMENT_SHADER)
	if err != nil {
		gl.DeleteShader(vertexId)
		return nil, err
	}
	id := gl.CreateProgram()
	if id == 0 {
		gl.DeleteShader(vertexId)
		gl.DeleteShader(fragmentId)
		return nil, errors.New("Unable to allocate program")
	}
	gl.AttachShader(id, vertexId)
	gl.AttachShader(id, fragmentId)

	var geomId uint32
	if opt.arbGeometryShader4.use {
		if !GetExtensions().GL_ARB_geometry_shader4() {
			gl.DeleteShader(vertexId)
			gl.DeleteShader(fragmentId)
			gl.DeleteProgram(id)
			return nil, errors.New("GL_ARB_geometry_shader4 requested, but extension is not available")
		}
		geomId, err = newShader(opt.arbGeometryShader4.source, gl.GEOMETRY_SHADER_ARB)
		if err != nil {
			gl.DeleteShader(vertexId)
			gl.DeleteShader(fragmentId)
			gl.DeleteProgram(id)
			return nil, err
		}
		defer func() {
			if rerr != nil {
				fmt.Printf("deleted geometry shader\n")
				gl.DeleteShader(geomId)
			}
		}()
		gl.AttachShader(id, geomId)
		gl.ProgramParameteriARB(id, gl.GEOMETRY_INPUT_TYPE_ARB, int32(opt.arbGeometryShader4.inType))
		gl.ProgramParameteriARB(id, gl.GEOMETRY_OUTPUT_TYPE_ARB, int32(opt.arbGeometryShader4.outType))
		gl.ProgramParameteriARB(id, gl.GEOMETRY_VERTICES_OUT_ARB, int32(opt.arbGeometryShader4.numOutputVertices))
	}

	gl.LinkProgram(id)
	var result int32
	gl.GetProgramiv(id, gl.LINK_STATUS, &result)
	if result == int32(gl.FALSE) {
		var loglength int32
		gl.GetProgramiv(id, gl.INFO_LOG_LENGTH, &loglength)
		log := make([]byte, loglength)
		var length int32
		gl.GetProgramInfoLog(id, loglength, &length, &log[0])
		gl.DeleteShader(vertexId)
		gl.DeleteShader(fragmentId)
		gl.DeleteProgram(id)
		return nil, fmt.Errorf("Unable to link program: %s", log[:length])
	}

	return &Program{
		id:       id,
		vertId:   vertexId,
		fragId:   fragmentId,
		uniforms: uniforms(id),
	}, nil
}

func newShader(source string, shaderType uint32) (uint32, error) {
	id := gl.CreateShader(shaderType)
	if id == 0 {
		return 0, errors.New("Unable to allocate shader")
	}
	ptr, free := convc.StringToC(source)
	defer free()
	gl.ShaderSource(id, 1, &ptr, nil)
	gl.CompileShader(id)

	var result int32
	gl.GetShaderiv(id, gl.COMPILE_STATUS, &result)
	if result == int32(gl.FALSE) {
		var loglength int32
		gl.GetShaderiv(id, gl.INFO_LOG_LENGTH, &loglength)
		log := make([]byte, loglength)
		var length int32
		gl.GetShaderInfoLog(id, loglength, &length, &log[0])
		gl.DeleteShader(id)
		return 0, fmt.Errorf("Unable to compile shader: %s", log[:length])
	}

	return id, nil
}

type Attrib struct {
	name     string
	location int32
}

func (program *Program) Attrib(attrname string) Attrib {
	location := gl.GetAttribLocation(program.id, gl.Str(attrname+"\x00"))
	return Attrib{
		name:     attrname,
		location: location,
	}
}

func (attrib Attrib) Name() string {
	return attrib.name
}

func (attrib Attrib) Valid() bool {
	return attrib.location != -1
}

func (attrib Attrib) Location() uint32 {
	if !attrib.Valid() {
		panic(fmt.Errorf("Could not find location for attribute '%s'", attrib.name))
	}
	return uint32(attrib.location)
}

func uniforms(id uint32) map[string]Uniform {
	var maxlength int32
	gl.GetProgramiv(id, gl.ACTIVE_UNIFORM_MAX_LENGTH, &maxlength)
	var numuniforms int32
	gl.GetProgramiv(id, gl.ACTIVE_UNIFORMS, &numuniforms)
	buf := make([]byte, maxlength)
	m := make(map[string]Uniform)
	for index := uint32(0); index < uint32(numuniforms); index++ {
		name, dt, siz := uniform(id, index, buf)
		loc := gl.GetUniformLocation(id, &buf[0])
		if loc == -1 {
			panic(fmt.Errorf("Expected location for indexed uniform '%s'", name))
		}
		m[name] = Uniform{
			name:     name,
			location: loc,
			typ:      dt,
			siz:      siz,
		}
	}
	return m
}

func uniform(id uint32, index uint32, buf []byte) (name string, datatype uint32, size int32) {
	var length int32
	var isize int32
	var idatatype uint32
	gl.GetActiveUniform(id, index, int32(len(buf)), &length, &isize, &idatatype, &buf[0])
	return string(buf[:length : length+1]), idatatype, isize
}

type Uniform struct {
	program  *Program
	name     string
	location int32
	typ      uint32
	siz      int32
}

func (program *Program) Uniform(uniformname string) Uniform {
	uniform, ok := program.uniforms[uniformname]
	if !ok {
		return Uniform{
			program:  program,
			name:     uniformname,
			location: -1,
		}
	}
	uniform.program = program
	return uniform
}

func (uniform Uniform) Name() string {
	return uniform.name
}

func (uniform Uniform) Valid() bool {
	return uniform.location != -1
}

func (uniform Uniform) Location() int32 {
	if !uniform.Valid() {
		panic(fmt.Errorf("Could not find location for uniform '%s'", uniform.name))
	}
	return int32(uniform.location)
}

func (uniform Uniform) SetSampler(data ...int32) {
	switch uniform.typ {
	case gl.SAMPLER, gl.SAMPLER_1D, gl.SAMPLER_2D, gl.SAMPLER_3D:
		gl.ProgramUniform1iv(uniform.program.id, uniform.Location(), uniform.siz, &data[0])
	default:
		panic(fmt.Errorf("Unusable gl type '%04X", uniform.typ))
	}
}

func (uniform Uniform) SetInt(data ...int32) {
	switch uniform.typ {
	case gl.INT:
		gl.ProgramUniform1iv(uniform.program.id, uniform.Location(), uniform.siz, &data[0])
	case gl.INT_VEC2:
		gl.ProgramUniform2iv(uniform.program.id, uniform.Location(), uniform.siz, &data[0])
	case gl.INT_VEC3:
		gl.ProgramUniform3iv(uniform.program.id, uniform.Location(), uniform.siz, &data[0])
	case gl.INT_VEC4:
		gl.ProgramUniform4iv(uniform.program.id, uniform.Location(), uniform.siz, &data[0])
	default:
		panic(fmt.Errorf("Unusable gl type '%04X", uniform.typ))
	}
}

func (uniform Uniform) SetFloat(data ...float32) {
	switch uniform.typ {
	case gl.FLOAT:
		gl.ProgramUniform1fv(uniform.program.id, uniform.Location(), uniform.siz, &data[0])
	case gl.FLOAT_VEC2:
		gl.ProgramUniform2fv(uniform.program.id, uniform.Location(), uniform.siz, &data[0])
	case gl.FLOAT_VEC3:
		gl.ProgramUniform3fv(uniform.program.id, uniform.Location(), uniform.siz, &data[0])
	case gl.FLOAT_VEC4:
		gl.ProgramUniform4fv(uniform.program.id, uniform.Location(), uniform.siz, &data[0])
	case gl.FLOAT_MAT2:
		gl.ProgramUniformMatrix2fv(uniform.program.id, uniform.location, uniform.siz, false, &data[0])
	case gl.FLOAT_MAT3:
		gl.ProgramUniformMatrix3fv(uniform.program.id, uniform.location, uniform.siz, false, &data[0])
	case gl.FLOAT_MAT4:
		gl.ProgramUniformMatrix4fv(uniform.program.id, uniform.location, uniform.siz, false, &data[0])
	default:
		panic(fmt.Errorf("Unusable gl type '%04X'", uniform.typ))
	}
}
<commit_msg>Always clean up shaders<commit_after>package gli

import (
	"errors"
	"fmt"

	"github.com/PieterD/glimmer/internal/convc"
	"github.com/go-gl/gl/v2.1/gl"
)

type Program struct {
	id       uint32
	uniforms map[string]Uniform
}

func (program *Program) Id() uint32 {
	return program.id
}

func (program *Program) Use() {
	gl.UseProgram(program.id)
}

func (program *Program) Delete() {
	gl.DeleteProgram(program.id)
}

type programOption struct {
	arbGeometryShader4 struct {
		use bool
		source string
		inType GeometryInputType
		outType GeometryOutputType
		numOutputVertices int
	}
}

type GeometryInputType uint32

const (
	GEOM_IN_POINTS GeometryInputType = gl.POINTS
	GEOM_IN_LINES GeometryInputType = gl.LINES
	GEOM_IN_LINES_ADJACENCY GeometryInputType = gl.LINES_ADJACENCY_ARB
	GEOM_IN_TRIANGLES GeometryInputType = gl.TRIANGLES
	GEOM_IN_TRIANGLES_ADJACENCY GeometryInputType = gl.TRIANGLES_ADJACENCY_ARB
)

type GeometryOutputType uint32

const (
	GEOM_OUT_POINTS GeometryOutputType = gl.POINTS
	GEOM_OUT_LINE_STRIP GeometryOutputType = gl.LINE_STRIP
	GEOM_OUT_TRIANGLE_STRIP GeometryOutputType = gl.TRIANGLE_STRIP
)

type ProgramOption func(opt *programOption)

func ProgramArbGeometryShader4(source string, inType GeometryInputType, outType GeometryOutputType, numOutputVertices int) ProgramOption {
	return func(opt *programOption) {
		opt.arbGeometryShader4.use = true
		opt.arbGeometryShader4.source = source
		opt.arbGeometryShader4.inType = inType
		opt.arbGeometryShader4.outType = outType
		opt.arbGeometryShader4.numOutputVertices = numOutputVertices
	}
}

func NewProgram(vertexSource, fragmentSource string, opts ...ProgramOption) (_ *Program, rerr error) {
	opt := programOption{}
	for _, o := range opts {
		o(&opt)
	}

	id := gl.CreateProgram()
	if id == 0 {
		return nil, errors.New("Unable to allocate program")
	}
	defer func() {
		if rerr != nil {
			gl.DeleteProgram(id)
		}
	}()

	vertexId, err := newShader(vertexSource, gl.VERTEX_SHADER)
	if err != nil {
		return nil, err
	}
	defer gl.DeleteShader(vertexId)
	gl.AttachShader(id, vertexId)

	fragmentId, err := newShader(fragmentSource, gl.FRAGMENT_SHADER)
	if err != nil {
		return nil, err
	}
	defer gl.DeleteShader(fragmentId)
	gl.AttachShader(id, fragmentId)

	var geomId uint32
	if opt.arbGeometryShader4.use {
		if !GetExtensions().GL_ARB_geometry_shader4() {
			return nil, errors.New("GL_ARB_geometry_shader4 requested, but extension is not available")
		}
		geomId, err = newShader(opt.arbGeometryShader4.source, gl.GEOMETRY_SHADER_ARB)
		if err != nil {
			return nil, err
		}
		defer gl.DeleteShader(geomId)
		gl.AttachShader(id, geomId)
		gl.ProgramParameteriARB(id, gl.GEOMETRY_INPUT_TYPE_ARB, int32(opt.arbGeometryShader4.inType))
		gl.ProgramParameteriARB(id, gl.GEOMETRY_OUTPUT_TYPE_ARB, int32(opt.arbGeometryShader4.outType))
		gl.ProgramParameteriARB(id, gl.GEOMETRY_VERTICES_OUT_ARB, int32(opt.arbGeometryShader4.numOutputVertices))
	}

	gl.LinkProgram(id)
	var result int32
	gl.GetProgramiv(id, gl.LINK_STATUS, &result)
	if result == int32(gl.FALSE) {
		var loglength int32
		gl.GetProgramiv(id, gl.INFO_LOG_LENGTH, &loglength)
		log := make([]byte, loglength)
		var length int32
		gl.GetProgramInfoLog(id, loglength, &length, &log[0])
		return nil, fmt.Errorf("Unable to link program: %s", log[:length])
	}

	return &Program{
		id:       id,
		uniforms: uniforms(id),
	}, nil
}

func newShader(source string, shaderType uint32) (uint32, error) {
	id := gl.CreateShader(shaderType)
	if id == 0 {
		return 0, errors.New("Unable to allocate shader")
	}
	ptr, free := convc.StringToC(source)
	defer free()
	gl.ShaderSource(id, 1, &ptr, nil)
	gl.CompileShader(id)

	var result int32
	gl.GetShaderiv(id, gl.COMPILE_STATUS, &result)
	if result == int32(gl.FALSE) {
		var loglength int32
		gl.GetShaderiv(id, gl.INFO_LOG_LENGTH, &loglength)
		log := make([]byte, loglength)
		var length int32
		gl.GetShaderInfoLog(id, loglength, &length, &log[0])
		gl.DeleteShader(id)
		return 0, fmt.Errorf("Unable to compile shader: %s", log[:length])
	}

	return id, nil
}

type Attrib struct {
	name     string
	location int32
}

func (program *Program) Attrib(attrname string) Attrib {
	location := gl.GetAttribLocation(program.id, gl.Str(attrname+"\x00"))
	return Attrib{
		name:     attrname,
		location: location,
	}
}

func (attrib Attrib) Name() string {
	return attrib.name
}

func (attrib Attrib) Valid() bool {
	return attrib.location != -1
}

func (attrib Attrib) Location() uint32 {
	if !attrib.Valid() {
		panic(fmt.Errorf("Could not find location for attribute '%s'", attrib.name))
	}
	return uint32(attrib.location)
}

func uniforms(id uint32) map[string]Uniform {
	var maxlength int32
	gl.GetProgramiv(id, gl.ACTIVE_UNIFORM_MAX_LENGTH, &maxlength)
	var numuniforms int32
	gl.GetProgramiv(id, gl.ACTIVE_UNIFORMS, &numuniforms)
	buf := make([]byte, maxlength)
	m := make(map[string]Uniform)
	for index := uint32(0); index < uint32(numuniforms); index++ {
		name, dt, siz := uniform(id, index, buf)
		loc := gl.GetUniformLocation(id, &buf[0])
		if loc == -1 {
			panic(fmt.Errorf("Expected location for indexed uniform '%s'", name))
		}
		m[name] = Uniform{
			name:     name,
			location: loc,
			typ:      dt,
			siz:      siz,
		}
	}
	return m
}

func uniform(id uint32, index uint32, buf []byte) (name string, datatype uint32, size int32) {
	var length int32
	var isize int32
	var idatatype uint32
	gl.GetActiveUniform(id, index, int32(len(buf)), &length, &isize, &idatatype, &buf[0])
	return string(buf[:length : length+1]), idatatype, isize
}

type Uniform struct {
	program  *Program
	name     string
	location int32
	typ      uint32
	siz      int32
}

func (program *Program) Uniform(uniformname string) Uniform {
	uniform, ok := program.uniforms[uniformname]
	if !ok {
		return Uniform{
			program:  program,
			name:     uniformname,
			location: -1,
		}
	}
	uniform.program = program
	return uniform
}

func (uniform Uniform) Name() string {
	return uniform.name
}

func (uniform Uniform) Valid() bool {
	return uniform.location != -1
}

func (uniform Uniform) Location() int32 {
	if !uniform.Valid() {
		panic(fmt.Errorf("Could not find location for uniform '%s'", uniform.name))
	}
	return int32(uniform.location)
}

func (uniform Uniform) SetSampler(data ...int32) {
	switch uniform.typ {
	case gl.SAMPLER, gl.SAMPLER_1D, gl.SAMPLER_2D, gl.SAMPLER_3D:
		gl.ProgramUniform1iv(uniform.program.id, uniform.Location(), uniform.siz, &data[0])
	default:
		panic(fmt.Errorf("Unusable gl type '%04X", uniform.typ))
	}
}

func (uniform Uniform) SetInt(data ...int32) {
	switch uniform.typ {
	case gl.INT:
		gl.ProgramUniform1iv(uniform.program.id, uniform.Location(), uniform.siz, &data[0])
	case gl.INT_VEC2:
		gl.ProgramUniform2iv(uniform.program.id, uniform.Location(), uniform.siz, &data[0])
	case gl.INT_VEC3:
		gl.ProgramUniform3iv(uniform.program.id, uniform.Location(), uniform.siz, &data[0])
	case gl.INT_VEC4:
		gl.ProgramUniform4iv(uniform.program.id, uniform.Location(), uniform.siz, &data[0])
	default:
		panic(fmt.Errorf("Unusable gl type '%04X", uniform.typ))
	}
}

func (uniform Uniform) SetFloat(data ...float32) {
	switch uniform.typ {
	case gl.FLOAT:
		gl.ProgramUniform1fv(uniform.program.id, uniform.Location(), uniform.siz, &data[0])
	case gl.FLOAT_VEC2:
		gl.ProgramUniform2fv(uniform.program.id, uniform.Location(), uniform.siz, &data[0])
	case gl.FLOAT_VEC3:
		gl.ProgramUniform3fv(uniform.program.id, uniform.Location(), uniform.siz, &data[0])
	case gl.FLOAT_VEC4:
		gl.ProgramUniform4fv(uniform.program.id, uniform.Location(), uniform.siz, &data[0])
	case gl.FLOAT_MAT2:
		gl.ProgramUniformMatrix2fv(uniform.program.id, uniform.location, uniform.siz, false, &data[0])
	case gl.FLOAT_MAT3:
		gl.ProgramUniformMatrix3fv(uniform.program.id, uniform.location, uniform.siz, false, &data[0])
	case gl.FLOAT_MAT4:
		gl.ProgramUniformMatrix4fv(uniform.program.id, uniform.location, uniform.siz, false, &data[0])
	default:
		panic(fmt.Errorf("Unusable gl type '%04X'", uniform.typ))
	}
}
<|endoftext|>


<commit_before>package git

import (
	"bytes"
	"fmt"
	"path/filepath"
	"strings"

	"github.com/polydawn/gosh"
	"go.polydawn.net/meep"

	"go.polydawn.net/repeatr/api/def"
	"go.polydawn.net/repeatr/rio"
)

/*
	Refer to, interact with, and manage a warehouse.

	Since this is git we're talking about, this is basically references
	to another git repo.
*/
type Warehouse struct {
	coord def.WarehouseCoord // user's string retained for messages
	url   string
}

/*
	Initialize a warehouse controller.

	Note: parsing git URLs is *hard*.  This function is "best effort"
	stuff and may not be able to error in all circumstances where the
	git commands will error.  See `man gitremote-helpers` for one reason
	this is the edge of a tarpit.  (This may improve as we do a better job
	of pinning specific git versions and sandboxing their environment,
	but at the moment, caveat emptor, and this is "PRs welcome" turf.)

	May panic with:
	  - Config Error: if the URI is unparsable or has an unsupported scheme.
*/
func NewWarehouse(coords rio.SiloURI) *Warehouse {
	wh := &Warehouse{
		coord: def.WarehouseCoord(coords),
		url:   hammerRelativePaths(string(coords)),
	}
	return wh
}

/*
	Desperately attempt to sanitize paths for git.

	This is almost certainly flawed; but frankly, so is git's handling of
	these things: it's not consistent across all commands (specifically,
	`git clone` and `git ls-remote`).  We're going for best-effort here,
	and slightly-better-than-not-trying -- low bars though those are.

	Git has a MAJORLY hard time consistently understanding local paths; so,
	we give up; internally all paths shall be absolutized because there's
	simply no functional choice.

	Here's a short list of issues we're concerned with:

	  - There's no "--" in ls-remote, so in order to avoid ambiguity with
	    arguments, we forbid things starting in "-".
	  - `git ls-remote .` and `git ls-remote ./` are a special case and will
	    return results even if the repo root is *above* your cwd, even
	    though git clone will disagree for obvious reasons.
	  - `git ls-remote ../sibling` works fine (and so does clone), but if
	    you do it from *inside* the same repo, it just says no-such-repo
	    (...but clone still works!)
	  - `ls-remote` in particular is really willing to take you on a ride:
	    if you have a repo-A, you make another git repo-B *inside* repo-A,
	    then you can `ls-remote ..` correctly....
	  - ....but if you then create a dir inside of repo-B, cd into it, and
	    try `git ls-remote ../` again you'll get the info from **repo-A**,
	    the grandparent.
	  - So basically, relative paths in git simply Don't Work and are
	    apparently Not Supported.

	Beyond that, we mostly punt.  It's essentially impossible to validate
	everything in advance in the same way that git will feel.
	The area we're *really* minding here is where we've observed
	`git clone` and `git ls-remote` behaviors to drift apart, because
	that's exceptionally nasty to deal with or report about clearly.
*/
func hammerRelativePaths(coords string) string {
	if len(coords) < 1 {
		return coords
	}
	// If things start with a "-", just... no.  Git lacks consistent ability
	//  to handle this unambiguously, so we're not even going to try.
	if coords[0] == '-' {
		panic(&def.ErrConfigValidation{Msg: "invalid git remote: cannot start with '-'"})
	}
	// If something looks like a relative path, absolutize it.
	//  There's a *litany* of issues this works around.
	//  If you had an ssh URL with a username starting in dot, eh, god help you.
	//  Note that we're not chasing after "file://" stuff here; git has
	//   even more other opinions about that already.
	if coords[0] == '.' {
		abs, err := filepath.Abs(coords)
		if err != nil {
			panic(meep.Meep(
				&rio.ErrInternal{Msg: "Failed handling local path"},
				meep.Cause(err),
			))
		}
		return abs
	}
	// There's no point in trying to parse the rest as a URL and sanitize it;
	//  it's both practically and theoretically impossible to accurately seek
	//   parity with how git may choose to see things.
	return coords
}

/*
	Check if the warehouse exists and can be contacted.

	Returns nil if contactable; if an error, the message will be
	an end-user-meaningful description of why the warehouse is out of reach.
*/
func (wh *Warehouse) Ping() error {
	// Shell out to git and ask it if it thinks there's a repo here.
	//  `git ls-remote` is our best option here for checking out that location and making sure it's advertising refs,
	//    while refraining from any alarmingly heavyweight operations or data transfers.
	var errBuf bytes.Buffer
	code := git.Bake(
		"ls-remote", wh.url,
		gosh.Opts{
			// never buffer stdout; it may be long and we don't care.
			Err:    &errBuf,
			OkExit: gosh.AnyExit,
		},
	).Run().GetExitCode()
	switch code {
	case 0:
		return nil
	case 128:
		// Code 128 appears to result from any cant-fetch scenario.
		// So far, we've also only seen error messages where the first line
		//  is interesting, so that's what we report.
		msg := strings.TrimPrefix(strings.SplitN(errBuf.String(), "\n", 2)[0], "fatal: ")
		// Known values include:
		//  - "'%s' does not appear to be a git repository"
		//  - "attempt to fetch/clone from a shallow repository"
		panic(&def.ErrWarehouseUnavailable{
			Msg:    fmt.Sprintf("git remote unavailable: %s", msg),
			During: "fetch",
			From:   wh.coord,
		})
	default:
		// We don't recognize this.
		panic(meep.Meep(
			&rio.ErrInternal{Msg: "Unknown git ls-remote error"},
			meep.Cause(fmt.Errorf("git exit code %d (stderr: %s)", code, errBuf.String())),
		))
	}
}
<commit_msg>Fix regression to git warehouse ping.<commit_after>package git

import (
	"bytes"
	"fmt"
	"path/filepath"
	"strings"

	"github.com/polydawn/gosh"
	"go.polydawn.net/meep"

	"go.polydawn.net/repeatr/api/def"
	"go.polydawn.net/repeatr/rio"
)

/*
	Refer to, interact with, and manage a warehouse.

	Since this is git we're talking about, this is basically references
	to another git repo.
*/
type Warehouse struct {
	coord def.WarehouseCoord // user's string retained for messages
	url   string
}

/*
	Initialize a warehouse controller.

	Note: parsing git URLs is *hard*.  This function is "best effort"
	stuff and may not be able to error in all circumstances where the
	git commands will error.  See `man gitremote-helpers` for one reason
	this is the edge of a tarpit.  (This may improve as we do a better job
	of pinning specific git versions and sandboxing their environment,
	but at the moment, caveat emptor, and this is "PRs welcome" turf.)

	May panic with:
	  - Config Error: if the URI is unparsable or has an unsupported scheme.
*/
func NewWarehouse(coords rio.SiloURI) *Warehouse {
	wh := &Warehouse{
		coord: def.WarehouseCoord(coords),
		url:   hammerRelativePaths(string(coords)),
	}
	return wh
}

/*
	Desperately attempt to sanitize paths for git.

	This is almost certainly flawed; but frankly, so is git's handling of
	these things: it's not consistent across all commands (specifically,
	`git clone` and `git ls-remote`).  We're going for best-effort here,
	and slightly-better-than-not-trying -- low bars though those are.

	Git has a MAJORLY hard time consistently understanding local paths; so,
	we give up; internally all paths shall be absolutized because there's
	simply no functional choice.

	Here's a short list of issues we're concerned with:

	  - There's no "--" in ls-remote, so in order to avoid ambiguity with
	    arguments, we forbid things starting in "-".
	  - `git ls-remote .` and `git ls-remote ./` are a special case and will
	    return results even if the repo root is *above* your cwd, even
	    though git clone will disagree for obvious reasons.
	  - `git ls-remote ../sibling` works fine (and so does clone), but if
	    you do it from *inside* the same repo, it just says no-such-repo
	    (...but clone still works!)
	  - `ls-remote` in particular is really willing to take you on a ride:
	    if you have a repo-A, you make another git repo-B *inside* repo-A,
	    then you can `ls-remote ..` correctly....
	  - ....but if you then create a dir inside of repo-B, cd into it, and
	    try `git ls-remote ../` again you'll get the info from **repo-A**,
	    the grandparent.
	  - So basically, relative paths in git simply Don't Work and are
	    apparently Not Supported.

	Beyond that, we mostly punt.  It's essentially impossible to validate
	everything in advance in the same way that git will feel.
	The area we're *really* minding here is where we've observed
	`git clone` and `git ls-remote` behaviors to drift apart, because
	that's exceptionally nasty to deal with or report about clearly.
*/
func hammerRelativePaths(coords string) string {
	if len(coords) < 1 {
		return coords
	}
	// If things start with a "-", just... no.  Git lacks consistent ability
	//  to handle this unambiguously, so we're not even going to try.
	if coords[0] == '-' {
		panic(&def.ErrConfigValidation{Msg: "invalid git remote: cannot start with '-'"})
	}
	// If something looks like a relative path, absolutize it.
	//  There's a *litany* of issues this works around.
	//  If you had an ssh URL with a username starting in dot, eh, god help you.
	//  Note that we're not chasing after "file://" stuff here; git has
	//   even more other opinions about that already.
	if coords[0] == '.' {
		abs, err := filepath.Abs(coords)
		if err != nil {
			panic(meep.Meep(
				&rio.ErrInternal{Msg: "Failed handling local path"},
				meep.Cause(err),
			))
		}
		return abs
	}
	// There's no point in trying to parse the rest as a URL and sanitize it;
	//  it's both practically and theoretically impossible to accurately seek
	//   parity with how git may choose to see things.
	return coords
}

/*
	Check if the warehouse exists and can be contacted.

	Returns nil if contactable; if an error, the message will be
	an end-user-meaningful description of why the warehouse is out of reach.
*/
func (wh *Warehouse) Ping() error {
	// Shell out to git and ask it if it thinks there's a repo here.
	//  `git ls-remote` is our best option here for checking out that location and making sure it's advertising refs,
	//    while refraining from any alarmingly heavyweight operations or data transfers.
	var errBuf bytes.Buffer
	code := git.Bake(
		"ls-remote", wh.url,
		gosh.Opts{
			// never buffer stdout; it may be long and we don't care.
			Err:    &errBuf,
			OkExit: gosh.AnyExit,
		},
	).Run().GetExitCode()
	switch code {
	case 0:
		return nil
	case 128:
		// Code 128 appears to result from any cant-fetch scenario.
		// So far, we've also only seen error messages where the first line
		//  is interesting, so that's what we report.
		msg := strings.TrimPrefix(strings.SplitN(errBuf.String(), "\n", 2)[0], "fatal: ")
		// Known values include:
		//  - "'%s' does not appear to be a git repository"
		//  - "attempt to fetch/clone from a shallow repository"
		return &def.ErrWarehouseUnavailable{
			Msg:    fmt.Sprintf("git remote unavailable: %s", msg),
			During: "fetch",
			From:   wh.coord,
		}
	default:
		// We don't recognize this.
		panic(meep.Meep(
			&rio.ErrInternal{Msg: "Unknown git ls-remote error"},
			meep.Cause(fmt.Errorf("git exit code %d (stderr: %s)", code, errBuf.String())),
		))
	}
}
<|endoftext|>


<commit_before>// package fronted provides a client and server for domain-fronted proxying
// using enproxy proxies.
package fronted

import (
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"fmt"
	"io"
	"net"
	"net/http"
	"strings"
	"sync"
	"time"

	"encoding/asn1"
	"github.com/getlantern/connpool"
	"github.com/getlantern/enproxy"
	"github.com/getlantern/golog"
	"github.com/getlantern/proxy"
	"github.com/getlantern/tlsdialer"
)

const (
	CONNECT = "CONNECT" // HTTP CONNECT method
)

var (
	log = golog.LoggerFor("fronted")

	// Cutoff for logging warnings about a dial having taken a long time.
	longDialLimit = 10 * time.Second

	// idleTimeout needs to be small enough that we stop using connections
	// before the upstream server/CDN closes them itself.
	// TODO: make this configurable.
	idleTimeout = 10 * time.Second
)

// Dialer is a domain-fronted proxy.Dialer.
type Dialer interface {
	proxy.Dialer

	// HttpClientUsing creates a simple domain-fronted HTTP client using the
	// specified Masquerade.
	HttpClientUsing(masquerade *Masquerade) *http.Client

	// NewDirectDomainFronter creates an HttpClient that domain-fronts but instead
	// of using enproxy proxies routes to the destination server directly from the
	// CDN. This is useful for web properties registered on the CDN itself, for
	// example geo.getiantem.org.
	NewDirectDomainFronter() *http.Client
}

// Config captures the configuration of a domain-fronted dialer.
type Config struct {
	// Host: the host (e.g. getiantem.org)
	Host string

	// Port: the port (e.g. 443)
	Port int

	// Masquerades: the Masquerades to use when domain-fronting. These will be
	// verified when the Dialer starts.
	Masquerades []*Masquerade

	// MaxMasquerades: the maximum number of masquerades to verify. If 0,
	// the masquerades are uncapped.
	MaxMasquerades int

	// PoolSize: if greater than 0, outbound connections will be pooled in an
	// eagerly loading connection pool. This can reduce latency when using
	// enproxy.
	PoolSize int

	// InsecureSkipVerify: if true, server's certificate is not verified.
	InsecureSkipVerify bool

	// RootCAs: optional CertPool specifying the root CAs to use for verifying
	// servers
	RootCAs *x509.CertPool

	// BufferRequests: if true, requests to the proxy will be buffered and sent
	// with identity encoding.  If false, they'll be streamed with chunked
	// encoding.
	BufferRequests bool

	// DialTimeoutMillis: how long to wait on dialing server before timing out
	// (defaults to 20 seconds)
	DialTimeoutMillis int

	// RedialAttempts: number of times to try redialing. The total number of
	// dial attempts will be 1 + RedialAttempts.
	RedialAttempts int

	// Weight: relative weight versus other servers (for round-robin)
	Weight int

	// QOS: relative quality of service offered. Should be >= 0, with higher
	// values indicating higher QOS.
	QOS int

	// OnDial: optional callback that gets invoked whenever we dial the server.
	// The Conn and error returned from this callback will be used in lieu of
	// the originals.
	OnDial func(conn net.Conn, err error) (net.Conn, error)

	// OnDialStats is an optional callback that will get called on every dial to
	// the server to report stats on what was dialed and how long each step
	// took.
	OnDialStats func(success bool, domain, addr string, resolutionTime, connectTime, handshakeTime time.Duration)
}

// dialer implements the proxy.Dialer interface by dialing domain-fronted
// servers.
type dialer struct {
	Config
	masquerades     *verifiedMasqueradeSet
	connPool        connpool.Pool
	enproxyConfig   *enproxy.Config
	tlsConfigs      map[string]*tls.Config
	tlsConfigsMutex sync.Mutex
}

// NewDialer creates a new Dialer for the given Config.
// WARNING - depending on configuration, this Dialer may contain a connection
// pool and/or a set of Masquerades that will leak resources.  Make sure to call
// Close() to clean these up when the Dialer is no longer in use.
func NewDialer(cfg Config) Dialer {
	d := &dialer{
		Config:     cfg,
		tlsConfigs: make(map[string]*tls.Config),
	}
	if d.Masquerades != nil {
		if d.MaxMasquerades == 0 {
			d.MaxMasquerades = len(d.Masquerades)
		}
		d.masquerades = d.verifiedMasquerades()
	}
	if cfg.PoolSize > 0 {
		d.connPool = connpool.New(connpool.Config{
			Size:         cfg.PoolSize,
			ClaimTimeout: idleTimeout,
			Dial:         d.dialServer,
		})
	}
	d.enproxyConfig = d.enproxyConfigWith(func(addr string) (net.Conn, error) {
		var conn net.Conn
		var err error
		if d.connPool != nil {
			conn, err = d.connPool.Get()
		} else {
			conn, err = d.dialServer()
		}
		if d.OnDial != nil {
			conn, err = d.OnDial(conn, err)
		}
		return conn, err
	})
	return d
}

// Dial dials upstream using domain-fronting.
func (d *dialer) Dial(network, addr string) (net.Conn, error) {
	if !strings.Contains(network, "tcp") {
		return nil, fmt.Errorf("Protocol %s is not supported, only tcp is supported", network)
	}

	return enproxy.Dial(addr, d.enproxyConfig)
}

// Close closes the dialer, in particular closing the underlying connection
// pool.
func (d *dialer) Close() error {
	if d.connPool != nil {
		// We close the connPool on a goroutine so as not to wait for Close to finish
		go d.connPool.Close()
	}
	if d.masquerades != nil {
		go d.masquerades.stop()
	}
	return nil
}

func (d *dialer) HttpClientUsing(masquerade *Masquerade) *http.Client {
	enproxyConfig := d.enproxyConfigWith(func(addr string) (net.Conn, error) {
		return d.dialServerWith(masquerade)
	})

	return &http.Client{
		Transport: &http.Transport{
			Dial: func(network, addr string) (net.Conn, error) {
				return enproxy.Dial(addr, enproxyConfig)
			},
		},
	}
}

type DirectDomainTransport struct {
	orig *http.Transport
}

func (ddf *DirectDomainTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
	// The connection is already encrypted by domain fronting.  We need to rewrite URLs starting
	// with "https://" to "http://", lest we get an error for doubling up on TLS.

	// The RoundTrip interface requires that we not modify the memory in the request, so we just
	// create a new one. Note this currently doesn't support request bodies.
	normalized := replacePrefix(req.URL.String(), "https://", "http://")
	norm, err := http.NewRequest(req.Method, normalized, nil)
	if err != nil {
		return nil, fmt.Errorf("Unable to construct request for url '%s' with error '%v'", normalized, err)
	}
	return ddf.orig.RoundTrip(norm)
}

func replacePrefix(s string, old string, new string) string {
	if strings.HasPrefix(s, old) {
		return new + strings.TrimPrefix(s, old)
	} else {
		return s
	}
}

func (ddf *DirectDomainTransport) CancelRequest(req *http.Request) {
	ddf.orig.CancelRequest(req)
}

func (ddf *DirectDomainTransport) CloseIdleConnections() {
	ddf.orig.CloseIdleConnections()
}

// Creates a new http.Client that does direct domain fronting.
func (d *dialer) NewDirectDomainFronter() *http.Client {
	return &http.Client{
		Transport: &DirectDomainTransport{
			orig: &http.Transport{
				Dial: func(network, addr string) (net.Conn, error) {
					log.Debugf("Dialing server with direct domain fronter")
					return d.dialServer()
				},
			},
		},
	}
}

func (d *dialer) enproxyConfigWith(dialProxy func(addr string) (net.Conn, error)) *enproxy.Config {
	return &enproxy.Config{
		DialProxy: dialProxy,
		NewRequest: func(upstreamHost string, method string, body io.Reader) (req *http.Request, err error) {
			if upstreamHost == "" {
				// No specific host requested, use configured one
				upstreamHost = d.Host
			}
			return http.NewRequest(method, "http://"+upstreamHost+"/", body)
		},
		BufferRequests: d.BufferRequests,
		IdleTimeout:    idleTimeout, // TODO: make this configurable
	}
}

func (d *dialer) dialServer() (net.Conn, error) {
	var masquerade *Masquerade
	if d.masquerades != nil {
		masquerade = d.masquerades.nextVerified()
	}
	return d.dialServerWith(masquerade)
}

func (d *dialer) dialServerWith(masquerade *Masquerade) (net.Conn, error) {
	dialTimeout := time.Duration(d.DialTimeoutMillis) * time.Millisecond
	if dialTimeout == 0 {
		dialTimeout = 20 * time.Second
	}

	// Note - we need to suppress the sending of the ServerName in the client
	// handshake to make host-spoofing work with Fastly.  If the client Hello
	// includes a server name, Fastly checks to make sure that this matches the
	// Host header in the HTTP request and if they don't match, it returns
	// a 400 Bad Request error.
	sendServerNameExtension := false

	cwt, err := tlsdialer.DialForTimings(
		&net.Dialer{
			Timeout: dialTimeout,
		},
		"tcp",
		d.addressForServer(masquerade),
		sendServerNameExtension,
		d.tlsConfig(masquerade))

	if d.OnDialStats != nil {
		domain := ""
		if masquerade != nil {
			domain = masquerade.Domain
		}

		resultAddr := ""
		if err == nil {
			resultAddr = cwt.Conn.RemoteAddr().String()
		}

		d.OnDialStats(err == nil, domain, resultAddr, cwt.ResolutionTime, cwt.ConnectTime, cwt.HandshakeTime)
	}

	if err != nil && masquerade != nil {
		err = fmt.Errorf("Unable to dial masquerade %s: %s", masquerade.Domain, err)
	}
	return cwt.Conn, err
}

// Get the address to dial for reaching the server
func (d *dialer) addressForServer(masquerade *Masquerade) string {
	return fmt.Sprintf("%s:%d", d.serverHost(masquerade), d.Port)
}

func (d *dialer) serverHost(masquerade *Masquerade) string {
	serverHost := d.Host
	if masquerade != nil {
		if masquerade.IpAddress != "" {
			serverHost = masquerade.IpAddress
		} else if masquerade.Domain != "" {
			serverHost = masquerade.Domain
		}
	}
	return serverHost
}

// tlsInfo is a temporary function that could help catching a bug. See this
// related PR: https://github.com/getlantern/lantern/issues/2398
func (d *dialer) tlsInfo(masquerade *Masquerade) string {
	var data []string

	serverName := d.Host

	if masquerade != nil {
		serverName = masquerade.Domain
	}

	var certpool []string
	subjects := d.RootCAs.Subjects()

	var dest pkix.RDNSequence

	for i, _ := range subjects {
		_, err := asn1.Unmarshal(subjects[i], &dest)
		if err != nil {
			certpool = append(certpool, fmt.Sprintf("Error[%d]: %q", i, err.Error()))
		} else {
			certpool = append(certpool, fmt.Sprintf("RDNSequence[%d]: %q", i, dest))
		}
	}

	data = append(data, fmt.Sprintf("Insecure Skip Verify: %v", d.InsecureSkipVerify))
	data = append(data, fmt.Sprintf("Host: %v", d.Host))
	data = append(data, fmt.Sprintf("Masquerade Domain: %v", masquerade.Domain))
	data = append(data, fmt.Sprintf("Server Name: %v", serverName))
	data = append(data, fmt.Sprintf("x509 cert pool subjects: %#s", strings.Join(certpool, " | ")))

	return strings.Join(data, ", ")
}

// tlsConfig builds a tls.Config for dialing the upstream host. Constructed
// tls.Configs are cached on a per-masquerade basis to enable client session
// caching and reduce the amount of PEM certificate parsing.
func (d *dialer) tlsConfig(masquerade *Masquerade) *tls.Config {
	d.tlsConfigsMutex.Lock()
	defer d.tlsConfigsMutex.Unlock()

	serverName := d.Host
	if masquerade != nil {
		serverName = masquerade.Domain
	}
	tlsConfig := d.tlsConfigs[serverName]
	if tlsConfig == nil {
		tlsConfig = &tls.Config{
			ClientSessionCache: tls.NewLRUClientSessionCache(1000),
			InsecureSkipVerify: d.InsecureSkipVerify,
			ServerName:         serverName,
			RootCAs:            d.RootCAs,
		}
		d.tlsConfigs[serverName] = tlsConfig
	}

	return tlsConfig
}
<commit_msg>increase DialTimeoutMillis default<commit_after>// package fronted provides a client and server for domain-fronted proxying
// using enproxy proxies.
package fronted

import (
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"fmt"
	"io"
	"net"
	"net/http"
	"strings"
	"sync"
	"time"

	"encoding/asn1"
	"github.com/getlantern/connpool"
	"github.com/getlantern/enproxy"
	"github.com/getlantern/golog"
	"github.com/getlantern/proxy"
	"github.com/getlantern/tlsdialer"
)

const (
	CONNECT = "CONNECT" // HTTP CONNECT method
)

var (
	log = golog.LoggerFor("fronted")

	// Cutoff for logging warnings about a dial having taken a long time.
	longDialLimit = 10 * time.Second

	// idleTimeout needs to be small enough that we stop using connections
	// before the upstream server/CDN closes them itself.
	// TODO: make this configurable.
	idleTimeout = 10 * time.Second
)

// Dialer is a domain-fronted proxy.Dialer.
type Dialer interface {
	proxy.Dialer

	// HttpClientUsing creates a simple domain-fronted HTTP client using the
	// specified Masquerade.
	HttpClientUsing(masquerade *Masquerade) *http.Client

	// NewDirectDomainFronter creates an HttpClient that domain-fronts but instead
	// of using enproxy proxies routes to the destination server directly from the
	// CDN. This is useful for web properties registered on the CDN itself, for
	// example geo.getiantem.org.
	NewDirectDomainFronter() *http.Client
}

// Config captures the configuration of a domain-fronted dialer.
type Config struct {
	// Host: the host (e.g. getiantem.org)
	Host string

	// Port: the port (e.g. 443)
	Port int

	// Masquerades: the Masquerades to use when domain-fronting. These will be
	// verified when the Dialer starts.
	Masquerades []*Masquerade

	// MaxMasquerades: the maximum number of masquerades to verify. If 0,
	// the masquerades are uncapped.
	MaxMasquerades int

	// PoolSize: if greater than 0, outbound connections will be pooled in an
	// eagerly loading connection pool. This can reduce latency when using
	// enproxy.
	PoolSize int

	// InsecureSkipVerify: if true, server's certificate is not verified.
	InsecureSkipVerify bool

	// RootCAs: optional CertPool specifying the root CAs to use for verifying
	// servers
	RootCAs *x509.CertPool

	// BufferRequests: if true, requests to the proxy will be buffered and sent
	// with identity encoding.  If false, they'll be streamed with chunked
	// encoding.
	BufferRequests bool

	// DialTimeoutMillis: how long to wait on dialing server before timing out
	// (defaults to 30 seconds)
	DialTimeoutMillis int

	// RedialAttempts: number of times to try redialing. The total number of
	// dial attempts will be 1 + RedialAttempts.
	RedialAttempts int

	// Weight: relative weight versus other servers (for round-robin)
	Weight int

	// QOS: relative quality of service offered. Should be >= 0, with higher
	// values indicating higher QOS.
	QOS int

	// OnDial: optional callback that gets invoked whenever we dial the server.
	// The Conn and error returned from this callback will be used in lieu of
	// the originals.
	OnDial func(conn net.Conn, err error) (net.Conn, error)

	// OnDialStats is an optional callback that will get called on every dial to
	// the server to report stats on what was dialed and how long each step
	// took.
	OnDialStats func(success bool, domain, addr string, resolutionTime, connectTime, handshakeTime time.Duration)
}

// dialer implements the proxy.Dialer interface by dialing domain-fronted
// servers.
type dialer struct {
	Config
	masquerades     *verifiedMasqueradeSet
	connPool        connpool.Pool
	enproxyConfig   *enproxy.Config
	tlsConfigs      map[string]*tls.Config
	tlsConfigsMutex sync.Mutex
}

// NewDialer creates a new Dialer for the given Config.
// WARNING - depending on configuration, this Dialer may contain a connection
// pool and/or a set of Masquerades that will leak resources.  Make sure to call
// Close() to clean these up when the Dialer is no longer in use.
func NewDialer(cfg Config) Dialer {
	d := &dialer{
		Config:     cfg,
		tlsConfigs: make(map[string]*tls.Config),
	}
	if d.Masquerades != nil {
		if d.MaxMasquerades == 0 {
			d.MaxMasquerades = len(d.Masquerades)
		}
		d.masquerades = d.verifiedMasquerades()
	}
	if cfg.PoolSize > 0 {
		d.connPool = connpool.New(connpool.Config{
			Size:         cfg.PoolSize,
			ClaimTimeout: idleTimeout,
			Dial:         d.dialServer,
		})
	}
	d.enproxyConfig = d.enproxyConfigWith(func(addr string) (net.Conn, error) {
		var conn net.Conn
		var err error
		if d.connPool != nil {
			conn, err = d.connPool.Get()
		} else {
			conn, err = d.dialServer()
		}
		if d.OnDial != nil {
			conn, err = d.OnDial(conn, err)
		}
		return conn, err
	})
	return d
}

// Dial dials upstream using domain-fronting.
func (d *dialer) Dial(network, addr string) (net.Conn, error) {
	if !strings.Contains(network, "tcp") {
		return nil, fmt.Errorf("Protocol %s is not supported, only tcp is supported", network)
	}

	return enproxy.Dial(addr, d.enproxyConfig)
}

// Close closes the dialer, in particular closing the underlying connection
// pool.
func (d *dialer) Close() error {
	if d.connPool != nil {
		// We close the connPool on a goroutine so as not to wait for Close to finish
		go d.connPool.Close()
	}
	if d.masquerades != nil {
		go d.masquerades.stop()
	}
	return nil
}

func (d *dialer) HttpClientUsing(masquerade *Masquerade) *http.Client {
	enproxyConfig := d.enproxyConfigWith(func(addr string) (net.Conn, error) {
		return d.dialServerWith(masquerade)
	})

	return &http.Client{
		Transport: &http.Transport{
			Dial: func(network, addr string) (net.Conn, error) {
				return enproxy.Dial(addr, enproxyConfig)
			},
		},
	}
}

type DirectDomainTransport struct {
	orig *http.Transport
}

func (ddf *DirectDomainTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
	// The connection is already encrypted by domain fronting.  We need to rewrite URLs starting
	// with "https://" to "http://", lest we get an error for doubling up on TLS.

	// The RoundTrip interface requires that we not modify the memory in the request, so we just
	// create a new one. Note this currently doesn't support request bodies.
	normalized := replacePrefix(req.URL.String(), "https://", "http://")
	norm, err := http.NewRequest(req.Method, normalized, nil)
	if err != nil {
		return nil, fmt.Errorf("Unable to construct request for url '%s' with error '%v'", normalized, err)
	}
	return ddf.orig.RoundTrip(norm)
}

func replacePrefix(s string, old string, new string) string {
	if strings.HasPrefix(s, old) {
		return new + strings.TrimPrefix(s, old)
	} else {
		return s
	}
}

func (ddf *DirectDomainTransport) CancelRequest(req *http.Request) {
	ddf.orig.CancelRequest(req)
}

func (ddf *DirectDomainTransport) CloseIdleConnections() {
	ddf.orig.CloseIdleConnections()
}

// Creates a new http.Client that does direct domain fronting.
func (d *dialer) NewDirectDomainFronter() *http.Client {
	return &http.Client{
		Transport: &DirectDomainTransport{
			orig: &http.Transport{
				Dial: func(network, addr string) (net.Conn, error) {
					log.Debugf("Dialing server with direct domain fronter")
					return d.dialServer()
				},
			},
		},
	}
}

func (d *dialer) enproxyConfigWith(dialProxy func(addr string) (net.Conn, error)) *enproxy.Config {
	return &enproxy.Config{
		DialProxy: dialProxy,
		NewRequest: func(upstreamHost string, method string, body io.Reader) (req *http.Request, err error) {
			if upstreamHost == "" {
				// No specific host requested, use configured one
				upstreamHost = d.Host
			}
			return http.NewRequest(method, "http://"+upstreamHost+"/", body)
		},
		BufferRequests: d.BufferRequests,
		IdleTimeout:    idleTimeout, // TODO: make this configurable
	}
}

func (d *dialer) dialServer() (net.Conn, error) {
	var masquerade *Masquerade
	if d.masquerades != nil {
		masquerade = d.masquerades.nextVerified()
	}
	return d.dialServerWith(masquerade)
}

func (d *dialer) dialServerWith(masquerade *Masquerade) (net.Conn, error) {
	dialTimeout := time.Duration(d.DialTimeoutMillis) * time.Millisecond
	if dialTimeout == 0 {
		dialTimeout = 30 * time.Second
	}

	// Note - we need to suppress the sending of the ServerName in the client
	// handshake to make host-spoofing work with Fastly.  If the client Hello
	// includes a server name, Fastly checks to make sure that this matches the
	// Host header in the HTTP request and if they don't match, it returns
	// a 400 Bad Request error.
	sendServerNameExtension := false

	cwt, err := tlsdialer.DialForTimings(
		&net.Dialer{
			Timeout: dialTimeout,
		},
		"tcp",
		d.addressForServer(masquerade),
		sendServerNameExtension,
		d.tlsConfig(masquerade))

	if d.OnDialStats != nil {
		domain := ""
		if masquerade != nil {
			domain = masquerade.Domain
		}

		resultAddr := ""
		if err == nil {
			resultAddr = cwt.Conn.RemoteAddr().String()
		}

		d.OnDialStats(err == nil, domain, resultAddr, cwt.ResolutionTime, cwt.ConnectTime, cwt.HandshakeTime)
	}

	if err != nil && masquerade != nil {
		err = fmt.Errorf("Unable to dial masquerade %s: %s", masquerade.Domain, err)
	}
	return cwt.Conn, err
}

// Get the address to dial for reaching the server
func (d *dialer) addressForServer(masquerade *Masquerade) string {
	return fmt.Sprintf("%s:%d", d.serverHost(masquerade), d.Port)
}

func (d *dialer) serverHost(masquerade *Masquerade) string {
	serverHost := d.Host
	if masquerade != nil {
		if masquerade.IpAddress != "" {
			serverHost = masquerade.IpAddress
		} else if masquerade.Domain != "" {
			serverHost = masquerade.Domain
		}
	}
	return serverHost
}

// tlsInfo is a temporary function that could help catching a bug. See this
// related PR: https://github.com/getlantern/lantern/issues/2398
func (d *dialer) tlsInfo(masquerade *Masquerade) string {
	var data []string

	serverName := d.Host

	if masquerade != nil {
		serverName = masquerade.Domain
	}

	var certpool []string
	subjects := d.RootCAs.Subjects()

	var dest pkix.RDNSequence

	for i, _ := range subjects {
		_, err := asn1.Unmarshal(subjects[i], &dest)
		if err != nil {
			certpool = append(certpool, fmt.Sprintf("Error[%d]: %q", i, err.Error()))
		} else {
			certpool = append(certpool, fmt.Sprintf("RDNSequence[%d]: %q", i, dest))
		}
	}

	data = append(data, fmt.Sprintf("Insecure Skip Verify: %v", d.InsecureSkipVerify))
	data = append(data, fmt.Sprintf("Host: %v", d.Host))
	data = append(data, fmt.Sprintf("Masquerade Domain: %v", masquerade.Domain))
	data = append(data, fmt.Sprintf("Server Name: %v", serverName))
	data = append(data, fmt.Sprintf("x509 cert pool subjects: %#s", strings.Join(certpool, " | ")))

	return strings.Join(data, ", ")
}

// tlsConfig builds a tls.Config for dialing the upstream host. Constructed
// tls.Configs are cached on a per-masquerade basis to enable client session
// caching and reduce the amount of PEM certificate parsing.
func (d *dialer) tlsConfig(masquerade *Masquerade) *tls.Config {
	d.tlsConfigsMutex.Lock()
	defer d.tlsConfigsMutex.Unlock()

	serverName := d.Host
	if masquerade != nil {
		serverName = masquerade.Domain
	}
	tlsConfig := d.tlsConfigs[serverName]
	if tlsConfig == nil {
		tlsConfig = &tls.Config{
			ClientSessionCache: tls.NewLRUClientSessionCache(1000),
			InsecureSkipVerify: d.InsecureSkipVerify,
			ServerName:         serverName,
			RootCAs:            d.RootCAs,
		}
		d.tlsConfigs[serverName] = tlsConfig
	}

	return tlsConfig
}
<|endoftext|>


<commit_before>package fronted

import (
	"crypto/tls"
	"crypto/x509"
	"errors"
	"fmt"
	"math/rand"
	"net"
	"net/http"
	"net/url"
	"strings"
	"sync"
	"time"

	"github.com/getlantern/tlsdialer"
)

var (
	poolCh      = make(chan *x509.CertPool, 1)
	candidateCh = make(chan *Masquerade, 1)
	masqCh      = make(chan *Masquerade, 1)
)

func Configure(pool *x509.CertPool, masquerades map[string][]*Masquerade) {
	if masquerades == nil || len(masquerades) == 0 {
		log.Errorf("No masquerades!!")
	}

	go func() {
		poolCh <- pool
		size := 0
		for _, arr := range masquerades {
			shuffle(arr)
			size += len(arr)
		}

		// Make an unblocke channel the same size as our group
		// of masquerades and push all of them into it.
		candidateCh = make(chan *Masquerade, size)
		log.Debugf("Adding %v candidates...", size)
		for _, arr := range masquerades {
			for _, m := range arr {
				candidateCh <- m
			}
		}
	}()
}

func shuffle(slc []*Masquerade) {
	n := len(slc)
	for i := 0; i < n; i++ {
		// choose index uniformly in [i, n-1]
		r := i + rand.Intn(n-i)
		slc[r], slc[i] = slc[i], slc[r]
	}
}

func getCertPool() *x509.CertPool {
	pool := <-poolCh
	if len(poolCh) == 0 {
		poolCh <- pool
	}
	return pool
}

type direct struct {
	tlsConfigs      map[string]*tls.Config
	tlsConfigsMutex sync.Mutex
}

func NewDirect() *direct {
	d := &direct{
		tlsConfigs: make(map[string]*tls.Config),
	}
	return d
}

// directTransport is a wrapper struct enabling us to modify the protocol of outgoing
// requests to make them all HTTP instead of potentially HTTPS, which breaks our particular
// implemenation of direct domain fronting.
type directTransport struct {
	http.Transport
}

func (ddf *directTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
	// The connection is already encrypted by domain fronting.  We need to rewrite URLs starting
	// with "https://" to "http://", lest we get an error for doubling up on TLS.

	// The RoundTrip interface requires that we not modify the memory in the request, so we just
	// create a copy.
	norm := new(http.Request)
	*norm = *req // includes shallow copies of maps, but okay
	norm.URL = new(url.URL)
	*norm.URL = *req.URL
	norm.URL.Scheme = "http"
	return ddf.Transport.RoundTrip(norm)
}

// NewHttpClient creates a new http.Client that does direct domain fronting.
func (d *direct) NewDirectHttpClient() *http.Client {
	trans := &directTransport{}
	trans.Dial = d.Dial
	trans.TLSHandshakeTimeout = 40 * time.Second
	trans.DisableKeepAlives = true
	return &http.Client{
		Transport: trans,
	}
}

// Dial persistently dials masquerades until one succeeds.
func (d *direct) Dial(network, addr string) (net.Conn, error) {
	for i := 0; i < 40; i++ {
		m := <-candidateCh
		log.Debugf("Dialing to %v", m)

		// We do the full TLS connection here because in practice the domains at a given IP
		// address can change frequently on CDNs, so the certificate may not match what
		// we expect.
		conn, err := d.dialServerWith(m)
		if err != nil {
			log.Debugf("Could not dial to %v, %v", m.IpAddress, err)
			// Don't re-add this candidate if it's any certificate error, as that
			// will just keep failing and will waste connections. We can't access the underlying
			// error at this point so just look for "certificate".
			if strings.Contains(err.Error(), "certificate") {
				log.Debugf("Continuing on certificate error")
			} else {
				candidateCh <- m
			}
		} else {
			log.Debugf("Got successful connection to: %v", m)
			candidateCh <- m
			return conn, nil
		}
	}
	return nil, errors.New("Could not dial any masquerade?")
}

func (d *direct) dialServerWith(masquerade *Masquerade) (net.Conn, error) {
	tlsConfig := d.tlsConfig(masquerade)
	dialTimeout := 30 * time.Second
	sendServerNameExtension := false

	cwt, err := tlsdialer.DialForTimings(
		&net.Dialer{
			Timeout: dialTimeout,
		},
		"tcp",
		masquerade.IpAddress+":443",
		sendServerNameExtension, // SNI or no
		tlsConfig)

	if err != nil && masquerade != nil {
		err = fmt.Errorf("Unable to dial masquerade %s: %s", masquerade.Domain, err)
	}
	return cwt.Conn, err
}

// tlsConfig builds a tls.Config for dialing the upstream host. Constructed
// tls.Configs are cached on a per-masquerade basis to enable client session
// caching and reduce the amount of PEM certificate parsing.
func (d *direct) tlsConfig(m *Masquerade) *tls.Config {
	d.tlsConfigsMutex.Lock()
	defer d.tlsConfigsMutex.Unlock()

	tlsConfig := d.tlsConfigs[m.Domain]
	if tlsConfig == nil {
		tlsConfig = &tls.Config{
			ClientSessionCache: tls.NewLRUClientSessionCache(1000),
			InsecureSkipVerify: false,
			ServerName:         m.Domain,
			RootCAs:            getCertPool(),
		}
		d.tlsConfigs[m.Domain] = tlsConfig
	}

	return tlsConfig
}
<commit_msg>fixed race condition<commit_after>package fronted

import (
	"crypto/tls"
	"crypto/x509"
	"errors"
	"fmt"
	"math/rand"
	"net"
	"net/http"
	"net/url"
	"strings"
	"sync"
	"time"

	"github.com/getlantern/tlsdialer"
)

var (
	poolCh      = make(chan *x509.CertPool, 1)
	candidateCh = make(chan *Masquerade, 1)
	masqCh      = make(chan *Masquerade, 1)
)

func Configure(pool *x509.CertPool, masquerades map[string][]*Masquerade) {
	if masquerades == nil || len(masquerades) == 0 {
		log.Errorf("No masquerades!!")
	}

	size := 0
	for _, arr := range masquerades {
		shuffle(arr)
		size += len(arr)
	}

	// Make an unblocke channel the same size as our group
	// of masquerades and push all of them into it.
	candidateCh = make(chan *Masquerade, size)

	go func() {
		log.Debugf("Adding %v candidates...", size)
		for _, arr := range masquerades {
			for _, m := range arr {
				candidateCh <- m
			}
		}
		poolCh <- pool
	}()
}

func shuffle(slc []*Masquerade) {
	n := len(slc)
	for i := 0; i < n; i++ {
		// choose index uniformly in [i, n-1]
		r := i + rand.Intn(n-i)
		slc[r], slc[i] = slc[i], slc[r]
	}
}

func getCertPool() *x509.CertPool {
	pool := <-poolCh
	if len(poolCh) == 0 {
		poolCh <- pool
	}
	return pool
}

type direct struct {
	tlsConfigs      map[string]*tls.Config
	tlsConfigsMutex sync.Mutex
}

func NewDirect() *direct {
	d := &direct{
		tlsConfigs: make(map[string]*tls.Config),
	}
	return d
}

// directTransport is a wrapper struct enabling us to modify the protocol of outgoing
// requests to make them all HTTP instead of potentially HTTPS, which breaks our particular
// implemenation of direct domain fronting.
type directTransport struct {
	http.Transport
}

func (ddf *directTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
	// The connection is already encrypted by domain fronting.  We need to rewrite URLs starting
	// with "https://" to "http://", lest we get an error for doubling up on TLS.

	// The RoundTrip interface requires that we not modify the memory in the request, so we just
	// create a copy.
	norm := new(http.Request)
	*norm = *req // includes shallow copies of maps, but okay
	norm.URL = new(url.URL)
	*norm.URL = *req.URL
	norm.URL.Scheme = "http"
	return ddf.Transport.RoundTrip(norm)
}

// NewHttpClient creates a new http.Client that does direct domain fronting.
func (d *direct) NewDirectHttpClient() *http.Client {
	trans := &directTransport{}
	trans.Dial = d.Dial
	trans.TLSHandshakeTimeout = 40 * time.Second
	trans.DisableKeepAlives = true
	return &http.Client{
		Transport: trans,
	}
}

// Dial persistently dials masquerades until one succeeds.
func (d *direct) Dial(network, addr string) (net.Conn, error) {
	for i := 0; i < 40; i++ {
		m := <-candidateCh
		log.Debugf("Dialing to %v", m)

		// We do the full TLS connection here because in practice the domains at a given IP
		// address can change frequently on CDNs, so the certificate may not match what
		// we expect.
		conn, err := d.dialServerWith(m)
		if err != nil {
			log.Debugf("Could not dial to %v, %v", m.IpAddress, err)
			// Don't re-add this candidate if it's any certificate error, as that
			// will just keep failing and will waste connections. We can't access the underlying
			// error at this point so just look for "certificate".
			if strings.Contains(err.Error(), "certificate") {
				log.Debugf("Continuing on certificate error")
			} else {
				candidateCh <- m
			}
		} else {
			log.Debugf("Got successful connection to: %v", m)
			candidateCh <- m
			return conn, nil
		}
	}
	return nil, errors.New("Could not dial any masquerade?")
}

func (d *direct) dialServerWith(masquerade *Masquerade) (net.Conn, error) {
	tlsConfig := d.tlsConfig(masquerade)
	dialTimeout := 30 * time.Second
	sendServerNameExtension := false

	cwt, err := tlsdialer.DialForTimings(
		&net.Dialer{
			Timeout: dialTimeout,
		},
		"tcp",
		masquerade.IpAddress+":443",
		sendServerNameExtension, // SNI or no
		tlsConfig)

	if err != nil && masquerade != nil {
		err = fmt.Errorf("Unable to dial masquerade %s: %s", masquerade.Domain, err)
	}
	return cwt.Conn, err
}

// tlsConfig builds a tls.Config for dialing the upstream host. Constructed
// tls.Configs are cached on a per-masquerade basis to enable client session
// caching and reduce the amount of PEM certificate parsing.
func (d *direct) tlsConfig(m *Masquerade) *tls.Config {
	d.tlsConfigsMutex.Lock()
	defer d.tlsConfigsMutex.Unlock()

	tlsConfig := d.tlsConfigs[m.Domain]
	if tlsConfig == nil {
		tlsConfig = &tls.Config{
			ClientSessionCache: tls.NewLRUClientSessionCache(1000),
			InsecureSkipVerify: false,
			ServerName:         m.Domain,
			RootCAs:            getCertPool(),
		}
		d.tlsConfigs[m.Domain] = tlsConfig
	}

	return tlsConfig
}
<|endoftext|>


<commit_before>package bugsnag

import (
	"encoding/json"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/grokify/chathooks/src/config"
	"github.com/grokify/chathooks/src/handlers"
	"github.com/grokify/chathooks/src/models"
	cc "github.com/grokify/commonchat"
	log "github.com/sirupsen/logrus"
)

const (
	DisplayName      = "Bugsnag"
	HandlerKey       = "bugsnag"
	MessageDirection = "out"
	MessageBodyType  = models.JSON
)

func NewHandler() handlers.Handler {
	return handlers.Handler{MessageBodyType: MessageBodyType, Normalize: Normalize}
}

/*
Bugsnag

**New error** in **production** from [Webpage]() in example:activity ([details]())

Error
ExampleError: An error occurred

Location
controllers/example.rb:11 - example_call
*/

func Normalize(cfg config.Configuration, bytes []byte) (cc.Message, error) {
	ccMsg := cc.NewMessage()
	iconURL, err := cfg.GetAppIconURL(HandlerKey)
	if err == nil {
		ccMsg.IconURL = iconURL.String()
	}

	ccMsg.Activity = fmt.Sprintf("%s alert", DisplayName)

	src, err := BugsnagOutMessageFromBytes(bytes)
	if err != nil {
		return ccMsg, err
	}

	parts := []string{}

	triggerMsg := strings.TrimSpace(src.Trigger.Message)
	if len(triggerMsg) > 0 {
		parts = append(parts, fmt.Sprintf("**%s**", triggerMsg))
	}

	stage := strings.TrimSpace(src.Release.ReleaseStage)
	if len(stage) > 0 {
		parts = append(parts, fmt.Sprintf("in **%s**", stage))
	}

	projLink := src.Project.MarkdownLink()
	if len(projLink) > 0 {
		parts = append(parts, fmt.Sprintf("from **%s**", projLink))
	}

	if len(src.Error.Context) > 0 {
		parts = append(parts, fmt.Sprintf("in %s", src.Error.Context))
	}

	if len(src.Error.Url) > 0 {
		details := fmt.Sprintf("([details](%s))", src.Error.Url)
		parts = append(parts, details)
	}

	if len(parts) > 0 {
		ccMsg.Title = strings.Join(parts, " ")
	}

	fields := []cc.Field{}

	if len(strings.TrimSpace(src.Error.Message)) > 0 {
		fields = append(fields, cc.Field{
			Title: "Error",
			Value: src.Error.Message})
	}

	for _, st := range src.Error.StackTrace {
		location := strings.TrimSpace(st.Location())
		if len(location) > 0 {
			fields = append(fields, cc.Field{
				Title: "Location",
				Value: location})
			break
		}
	}

	if len(strings.TrimSpace(src.Error.Status)) > 0 {
		status := src.Error.Status
		if strings.ToLower(strings.TrimSpace(status)) == "open" {
			unhandled := src.Error.Unhandled
			if unhandled {
				status += " - unhandled"
			} else {
				status += " - handled"
			}
		}

		fields = append(fields, cc.Field{
			Title: "Status",
			Value: status})
	}

	if len(fields) > 0 {
		attachment := cc.NewAttachment()
		for _, field := range fields {
			attachment.AddField(field)
		}
		ccMsg.AddAttachment(attachment)
	}

	/*
				if len(src.ActorName) > 0 {
					ccMsg.Activity = src.ActorName
				}

				attachment := cc.NewAttachment()

				if len(src.Model.Subject) > 0 {
					attachment.Text = src.Model.Subject
				}
				if len(src.Model.State) > 0 {
					attachment.AddField(cc.Field{
						Title: "State",
						Value: stringsutil.ToUpperFirst(src.Model.State)})
				}
				ccMsg.AddAttachment(attachment)

		   "stackTrace":[
		     {
		       "inProject":true,
		       "lineNumber":1234,
		       "columnNumber":123,
		       "file":"controllers/auth/session_controller.rb",
		       "method":"create",
		       "code":{
		         "1231":"  def a",
		         "1232":"",
		         "1233":"    if problem?",
		         "1234":"      raise 'something went wrong'",
		         "1235":"    end",
		         "1236":"",
		         "1237":"  end"
		       }
		     }
		   ]
	*/

	return ccMsg, nil
}

func BugsnagOutMessageFromBytes(bytes []byte) (BugsnagOutMessage, error) {
	log.WithFields(log.Fields{
		"type":    "message.raw",
		"message": string(bytes),
	}).Debug(fmt.Sprintf("%v message.", DisplayName))
	msg := BugsnagOutMessage{}
	err := json.Unmarshal(bytes, &msg)
	if err != nil {
		log.WithFields(log.Fields{
			"type":  "message.json.unmarshal",
			"error": fmt.Sprintf("%v\n", err),
		}).Warn(fmt.Sprintf("%v request unmarshal failure.", DisplayName))
	}
	return msg, err
}

type BugsnagOutMessage struct {
	Account BugsnagAccount `json:"account,omitempty"`
	Project BugsnagProject `json:"project,omitempty"`
	Trigger BugsnagTrigger `json:"trigger,omitempty"`
	Comment BugsnagComment `json:"created_at,omitempty"`
	User    BugsnagUser    `json:"user,omitempty"`
	Error   BugsnagError   `json:"error,omitempty"`
	Release BugsnagRelease `json:"release,omitempty"`
}

func (msg *BugsnagOutMessage) ReleaseStage() string {
	relRelStage := strings.TrimSpace(msg.Release.ReleaseStage)
	appRelStage := strings.TrimSpace(msg.Error.App.ReleaseStage)
	if len(appRelStage) > 0 {
		return appRelStage
	}
	return relRelStage
}

type BugsnagAccount struct {
	Id   string `json:"id,omitempty"`
	Name string `json:"name,omitempty"`
	URL  string `json:"url,omitempty"`
}

type BugsnagProject struct {
	Id   string `json:"id,omitempty"`
	Name string `json:"name,omitempty"`
	URL  string `json:"url,omitempty"`
}

func (bp *BugsnagProject) MarkdownLink() string {
	if len(bp.Name) > 0 && len(bp.URL) > 0 {
		return fmt.Sprintf("[%s](%s)", bp.Name, bp.URL)
	} else if len(bp.URL) > 0 {
		return fmt.Sprintf("[%s](%s)", bp.URL, bp.URL)
	}
	return ""
}

type BugsnagTrigger struct {
	Type        string            `json:"type,omitempty"`
	Message     string            `json:"message,omitempty"`
	SnoozeRule  BugsnagSnoozeRule `json:"snoozeRule,omitempty"`
	Rate        int32             `json:"rate,omitempty"`
	StateChange string            `json:"stateChange,omitempty"`
}

type BugsnagSnoozeRule struct {
	Type      string `json:"type,omitempty"`
	RuleValue int32  `json:"ruleValue,omitempty"`
}

type BugsnagComment struct{}

type BugsnagUser struct {
	Id    string `json:"id,omitempty"`
	Name  string `json:"name,omitempty"`
	Email string `json:"email,omitempty"`
}

type BugsnagRelease struct {
	Id            string               `json:"id,omitempty"`
	Version       string               `json:"version,omitempty"`
	VersionCode   string               `json:"versionCode,omitempty"`
	BundleVersion string               `json:"bundleVersion,omitempty"`
	ReleaseStage  string               `json:"releaseStage,omitempty"`
	URL           string               `json:"url,omitempty"`
	ReleaseTime   time.Time            `json:"releaseTime,omitempty"`
	ReleasedBy    string               `json:"releasedBy,omitempty"`
	SourceControl BugsnagSourceControl `json:"sourceControl,omitempty"`
	Metadata      map[string]string    `json:"metadata,omitempty"`
}

type BugsnagSourceControl struct {
	Provider    string `json:"provider,omitempty"`
	Revision    string `json:"revision,omitempty"`
	RevisionURL string `json:"revisionUrl,omitempty"`
	DiffURL     string `json:"diffUrl,omitempty"`
}

type BugsnagError struct {
	Id             string                   `json:"id,omitempty"`
	ErrorId        string                   `json:"errorId,omitempty"`
	ExceptionClass string                   `json:"exceptionClass,omitempty"`
	Message        string                   `json:"message,omitempty"`
	Context        string                   `json:"context,omitempty"`
	FirstReceived  time.Time                `json:"firstReceived,omitempty"`
	ReceivedAt     time.Time                `json:"receivedAt,omitempty"`
	RequestUrl     string                   `json:"requestUrl,omitempty"`
	AssignedUserId string                   `json:"assignedUserId,omitempty"`
	Url            string                   `json:"url,omitempty"`
	Severity       string                   `json:"severity,omitempty"`
	Status         string                   `json:"status,omitempty"`
	Unhandled      bool                     `json:"unhandled,omitempty"`
	CreatedIssue   BugsnagErrorIssue        `json:"createdIssue,omitempty"`
	User           BugsnagUser              `json:"user,omitempty"`
	App            BugsnaggErrorApp         `json:"app,omitempty"`
	Device         BugsnagErrorDevice       `json:"device,omitempty"`
	StackTrace     []BugsnagErrorStackTrace `json:"stackTrace,omitempty"`
}

type BugsnagErrorIssue struct {
	Id     string `json:"id,omitempty"`
	Number int    `json:"name,omitempty"`
	Type   string `json:"type,omitempty"`
	URL    string `json:"url,omitempty"`
}

type BugsnaggErrorApp struct {
	Id                   string   `json:"id,omitempty"`
	Version              string   `json:"version,omitempty"`
	VersionCode          string   `json:"versionCode,omitempty"`
	BundleVersion        string   `json:"bundleVersion,omitempty"`
	CodeBundleId         string   `json:"codeBundleId,omitempty"`
	BuildUUID            string   `json:"buildUUID,omitempty"`
	ReleaseStage         string   `json:"releaseStage,omitempty"`
	Type                 string   `json:"type,omitempty"`
	DsymUUIDs            []string `json:"dsymUUIDs,omitempty"`
	Duration             int      `json:"duration,omitempty"`
	durationInForeground int      `json:"durationInForeground,omitempty"`
	inForeground         bool     `json:"inForeground,omitempty"`
}

type BugsnagErrorDevice struct {
	Id             string    `json:"id,omitempty"`
	Manufacturer   string    `json:"manufacturer,omitempty"`
	Model          string    `json:"model,omitempty"`
	ModelNumber    string    `json:"modelNumber,omitempty"`
	OsName         string    `json:"osName,omitempty"`
	OsVersion      string    `json:"osVersion,omitempty"`
	FreeMemory     int       `json:"freeMemory,omitempty"`
	TotalMemory    int       `json:"totalMemory,omitempty"`
	FreeDisk       int       `json:"freeDisk,omitempty"`
	BrowserName    string    `json:"browserName,omitempty"`
	BrowserVersion string    `json:"browserVersion,omitempty"`
	Jailbroken     bool      `json:"jailbroken,omitempty"`
	Orientation    string    `json:"orientation,omitempty"`
	Locale         string    `json:"locale,omitempty"`
	Charging       bool      `json:"charging,omitempty"`
	BatteryLevel   float32   `json:"batteryLevel,omitempty"`
	Time           time.Time `json:"time,omitempty"`
	Timezone       string    `json:"timezone,omitempty"`
}

type BugsnagErrorStackTrace struct {
	InProject    bool              `json:"inProject,omitempty"`
	LineNumber   int               `json:"lineNumber,omitempty"`
	ColumnNumber int               `json:"columnNumber,omitempty"`
	File         string            `json:"file,omitempty"`
	Method       string            `json:"method,omitempty"`
	Code         map[string]string `json:"code,omitempty"`
}

// Location returns a string per the Slack integration
func (st *BugsnagErrorStackTrace) Location() string {
	location := ""
	st.File = strings.TrimSpace(st.File)
	st.Method = strings.TrimSpace(st.Method)
	if len(st.File) > 0 {
		location = st.File
		if st.LineNumber > 0 {
			location += ":" + strconv.Itoa(st.LineNumber)
		}
		if len(st.Method) > 0 {
			location += " - " + st.Method
		}
	}
	return location
}

/*

controllers/example.rb:11 - example_call

   "stackTrace":[
     {
       "inProject":true,
       "lineNumber":1234,
       "columnNumber":123,
       "file":"controllers/auth/session_controller.rb",
       "method":"create",
       "code":{
         "1231":"  def a",
         "1232":"",
         "1233":"    if problem?",
         "1234":"      raise 'something went wrong'",
         "1235":"    end",
         "1236":"",
         "1237":"  end"
       }
     }
   ]
*/
<commit_msg>refactor<commit_after>package bugsnag

import (
	"encoding/json"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/grokify/chathooks/src/config"
	"github.com/grokify/chathooks/src/handlers"
	"github.com/grokify/chathooks/src/models"
	cc "github.com/grokify/commonchat"
	log "github.com/sirupsen/logrus"
)

const (
	DisplayName      = "Bugsnag"
	HandlerKey       = "bugsnag"
	MessageDirection = "out"
	MessageBodyType  = models.JSON
)

func NewHandler() handlers.Handler {
	return handlers.Handler{MessageBodyType: MessageBodyType, Normalize: Normalize}
}

/*
Bugsnag

**New error** in **production** from [Webpage]() in example:activity ([details]())

Error
ExampleError: An error occurred

Location
controllers/example.rb:11 - example_call
*/

func Normalize(cfg config.Configuration, bytes []byte) (cc.Message, error) {
	ccMsg := cc.NewMessage()
	iconURL, err := cfg.GetAppIconURL(HandlerKey)
	if err == nil {
		ccMsg.IconURL = iconURL.String()
	}

	ccMsg.Activity = fmt.Sprintf("%s alert", DisplayName)

	src, err := BugsnagOutMessageFromBytes(bytes)
	if err != nil {
		return ccMsg, err
	}

	textMessage := buildTextMessage(src)
	if len(textMessage) > 0 {
		ccMsg.Title = textMessage
	}

	fields := []cc.Field{}

	if len(strings.TrimSpace(src.Error.Message)) > 0 {
		fields = append(fields, cc.Field{
			Title: "Error",
			Value: src.Error.Message})
	}

	for _, st := range src.Error.StackTrace {
		location := strings.TrimSpace(st.Location())
		if len(location) > 0 {
			fields = append(fields, cc.Field{
				Title: "Location",
				Value: location})
			break
		}
	}

	if len(strings.TrimSpace(src.Error.Status)) > 0 {
		status := src.Error.Status
		if strings.ToLower(strings.TrimSpace(status)) == "open" {
			unhandled := src.Error.Unhandled
			if unhandled {
				status += " - unhandled"
			} else {
				status += " - handled"
			}
		}

		if len(src.Error.Url) > 0 {
			status = fmt.Sprintf("[%s](%s)", status, src.Error.Url)
		}

		fields = append(fields, cc.Field{
			Title: "Status",
			Value: status})
	}

	if len(fields) > 0 {
		attachment := cc.NewAttachment()
		for _, field := range fields {
			attachment.AddField(field)
		}
		ccMsg.AddAttachment(attachment)
	}

	return ccMsg, nil
}

func buildTextMessage(src BugsnagOutMessage) string {
	parts := []string{}

	triggerMsg := strings.TrimSpace(src.Trigger.Message)
	if len(triggerMsg) > 0 {
		parts = append(parts, fmt.Sprintf("**%s**", triggerMsg))
	}

	stage := strings.TrimSpace(src.Release.ReleaseStage)
	if len(stage) > 0 {
		parts = append(parts, fmt.Sprintf("in **%s**", stage))
	}

	projLink := src.Project.MarkdownLink()
	if len(projLink) > 0 {
		parts = append(parts, fmt.Sprintf("from **%s**", projLink))
	}

	if len(src.Error.Context) > 0 {
		parts = append(parts, fmt.Sprintf("in %s", src.Error.Context))
	}

	if len(src.Error.Url) > 0 {
		details := fmt.Sprintf("([details](%s))", src.Error.Url)
		parts = append(parts, details)
	}

	return strings.Join(parts, " ")
}

func BugsnagOutMessageFromBytes(bytes []byte) (BugsnagOutMessage, error) {
	log.WithFields(log.Fields{
		"type":    "message.raw",
		"message": string(bytes),
	}).Debug(fmt.Sprintf("%v message.", DisplayName))
	msg := BugsnagOutMessage{}
	err := json.Unmarshal(bytes, &msg)
	if err != nil {
		log.WithFields(log.Fields{
			"type":  "message.json.unmarshal",
			"error": fmt.Sprintf("%v\n", err),
		}).Warn(fmt.Sprintf("%v request unmarshal failure.", DisplayName))
	}
	return msg, err
}

type BugsnagOutMessage struct {
	Account BugsnagAccount `json:"account,omitempty"`
	Project BugsnagProject `json:"project,omitempty"`
	Trigger BugsnagTrigger `json:"trigger,omitempty"`
	Comment BugsnagComment `json:"created_at,omitempty"`
	User    BugsnagUser    `json:"user,omitempty"`
	Error   BugsnagError   `json:"error,omitempty"`
	Release BugsnagRelease `json:"release,omitempty"`
}

func (msg *BugsnagOutMessage) ReleaseStage() string {
	relRelStage := strings.TrimSpace(msg.Release.ReleaseStage)
	appRelStage := strings.TrimSpace(msg.Error.App.ReleaseStage)
	if len(appRelStage) > 0 {
		return appRelStage
	}
	return relRelStage
}

type BugsnagAccount struct {
	Id   string `json:"id,omitempty"`
	Name string `json:"name,omitempty"`
	URL  string `json:"url,omitempty"`
}

type BugsnagProject struct {
	Id   string `json:"id,omitempty"`
	Name string `json:"name,omitempty"`
	URL  string `json:"url,omitempty"`
}

func (bp *BugsnagProject) MarkdownLink() string {
	if len(bp.Name) > 0 && len(bp.URL) > 0 {
		return fmt.Sprintf("[%s](%s)", bp.Name, bp.URL)
	} else if len(bp.URL) > 0 {
		return fmt.Sprintf("[%s](%s)", bp.URL, bp.URL)
	}
	return ""
}

type BugsnagTrigger struct {
	Type        string            `json:"type,omitempty"`
	Message     string            `json:"message,omitempty"`
	SnoozeRule  BugsnagSnoozeRule `json:"snoozeRule,omitempty"`
	Rate        int32             `json:"rate,omitempty"`
	StateChange string            `json:"stateChange,omitempty"`
}

type BugsnagSnoozeRule struct {
	Type      string `json:"type,omitempty"`
	RuleValue int32  `json:"ruleValue,omitempty"`
}

type BugsnagComment struct{}

type BugsnagUser struct {
	Id    string `json:"id,omitempty"`
	Name  string `json:"name,omitempty"`
	Email string `json:"email,omitempty"`
}

type BugsnagRelease struct {
	Id            string               `json:"id,omitempty"`
	Version       string               `json:"version,omitempty"`
	VersionCode   string               `json:"versionCode,omitempty"`
	BundleVersion string               `json:"bundleVersion,omitempty"`
	ReleaseStage  string               `json:"releaseStage,omitempty"`
	URL           string               `json:"url,omitempty"`
	ReleaseTime   time.Time            `json:"releaseTime,omitempty"`
	ReleasedBy    string               `json:"releasedBy,omitempty"`
	SourceControl BugsnagSourceControl `json:"sourceControl,omitempty"`
	Metadata      map[string]string    `json:"metadata,omitempty"`
}

type BugsnagSourceControl struct {
	Provider    string `json:"provider,omitempty"`
	Revision    string `json:"revision,omitempty"`
	RevisionURL string `json:"revisionUrl,omitempty"`
	DiffURL     string `json:"diffUrl,omitempty"`
}

type BugsnagError struct {
	Id             string                   `json:"id,omitempty"`
	ErrorId        string                   `json:"errorId,omitempty"`
	ExceptionClass string                   `json:"exceptionClass,omitempty"`
	Message        string                   `json:"message,omitempty"`
	Context        string                   `json:"context,omitempty"`
	FirstReceived  time.Time                `json:"firstReceived,omitempty"`
	ReceivedAt     time.Time                `json:"receivedAt,omitempty"`
	RequestUrl     string                   `json:"requestUrl,omitempty"`
	AssignedUserId string                   `json:"assignedUserId,omitempty"`
	Url            string                   `json:"url,omitempty"`
	Severity       string                   `json:"severity,omitempty"`
	Status         string                   `json:"status,omitempty"`
	Unhandled      bool                     `json:"unhandled,omitempty"`
	CreatedIssue   BugsnagErrorIssue        `json:"createdIssue,omitempty"`
	User           BugsnagUser              `json:"user,omitempty"`
	App            BugsnaggErrorApp         `json:"app,omitempty"`
	Device         BugsnagErrorDevice       `json:"device,omitempty"`
	StackTrace     []BugsnagErrorStackTrace `json:"stackTrace,omitempty"`
}

type BugsnagErrorIssue struct {
	Id     string `json:"id,omitempty"`
	Number int    `json:"name,omitempty"`
	Type   string `json:"type,omitempty"`
	URL    string `json:"url,omitempty"`
}

type BugsnaggErrorApp struct {
	Id                   string   `json:"id,omitempty"`
	Version              string   `json:"version,omitempty"`
	VersionCode          string   `json:"versionCode,omitempty"`
	BundleVersion        string   `json:"bundleVersion,omitempty"`
	CodeBundleId         string   `json:"codeBundleId,omitempty"`
	BuildUUID            string   `json:"buildUUID,omitempty"`
	ReleaseStage         string   `json:"releaseStage,omitempty"`
	Type                 string   `json:"type,omitempty"`
	DsymUUIDs            []string `json:"dsymUUIDs,omitempty"`
	Duration             int      `json:"duration,omitempty"`
	durationInForeground int      `json:"durationInForeground,omitempty"`
	inForeground         bool     `json:"inForeground,omitempty"`
}

type BugsnagErrorDevice struct {
	Id             string    `json:"id,omitempty"`
	Manufacturer   string    `json:"manufacturer,omitempty"`
	Model          string    `json:"model,omitempty"`
	ModelNumber    string    `json:"modelNumber,omitempty"`
	OsName         string    `json:"osName,omitempty"`
	OsVersion      string    `json:"osVersion,omitempty"`
	FreeMemory     int       `json:"freeMemory,omitempty"`
	TotalMemory    int       `json:"totalMemory,omitempty"`
	FreeDisk       int       `json:"freeDisk,omitempty"`
	BrowserName    string    `json:"browserName,omitempty"`
	BrowserVersion string    `json:"browserVersion,omitempty"`
	Jailbroken     bool      `json:"jailbroken,omitempty"`
	Orientation    string    `json:"orientation,omitempty"`
	Locale         string    `json:"locale,omitempty"`
	Charging       bool      `json:"charging,omitempty"`
	BatteryLevel   float32   `json:"batteryLevel,omitempty"`
	Time           time.Time `json:"time,omitempty"`
	Timezone       string    `json:"timezone,omitempty"`
}

type BugsnagErrorStackTrace struct {
	InProject    bool              `json:"inProject,omitempty"`
	LineNumber   int               `json:"lineNumber,omitempty"`
	ColumnNumber int               `json:"columnNumber,omitempty"`
	File         string            `json:"file,omitempty"`
	Method       string            `json:"method,omitempty"`
	Code         map[string]string `json:"code,omitempty"`
}

// Location returns a string per the Slack integration
func (st *BugsnagErrorStackTrace) Location() string {
	location := ""
	st.File = strings.TrimSpace(st.File)
	st.Method = strings.TrimSpace(st.Method)
	if len(st.File) > 0 {
		location = st.File
		if st.LineNumber > 0 {
			location += ":" + strconv.Itoa(st.LineNumber)
		}
		if len(st.Method) > 0 {
			location += " - " + st.Method
		}
	}
	return location
}
<|endoftext|>