<commit_before>package main

import (
	"crypto/md5"
	"crypto/sha1"
	"crypto/sha256"
	"crypto/sha512"
	"errors"
	"flag"
	"fmt"
	"hash"
	"io"
	"mime"
	"os"
	"path/filepath"
	"regexp"

	"github.com/michielbuddingh/spamsum"
)

func message() {
	fmt.Println("Demisto uploader to create investigations and entries from directories")
	fmt.Println("======================================================================")
}

var (
	path          = flag.String("f", ".", "Folder to recursively iterage")
	username      = flag.String("u", "", "Username to login to the server")
	password      = flag.String("p", "", "Password to login to the server")
	server        = flag.String("s", "", "Demisto server URL")
	investigation = flag.String("investigation", "", "If provided, investigation ID to use instead of creating investigations")
	regex         = flag.String("regex", "", "Regex to filter files and folders. If provided, only files matching the regex will be evaluated and metadata uploaded.")
	verbose       = flag.Bool("v", true, "Verbose mode - should we print directories we are handling")
	extraVerbose  = flag.Bool("vv", false, "Very verbose - should we print details about every file")
)

var (
	r *regexp.Regexp
)

func check() {
	/*
		if *username == "" {
			fmt.Fprintln(os.Stderr, "Please provide the username")
			os.Exit(1)
		}
		if *password == "" {
			fmt.Fprintln(os.Stderr, "Please provide the password")
			os.Exit(1)
		}
		if *server == "" {
			fmt.Fprintln(os.Stderr, "Please provide the Demisto server URL")
			os.Exit(1)
		}*/
	if *regex != "" {
		var err error
		r, err = regexp.Compile(*regex)
		if err != nil {
			fmt.Fprintf(os.Stderr, "Invalid regex - %v\n", err)
			os.Exit(1)
		}
	}
}

// fileInfo holds information about a file.
type fileInfo struct {
	Created     int64
	CreatedStr  string
	Accessed    int64
	AccessedStr string
	Changed     int64
	ChangedStr  string
	Path        string
	Type        string
	Size        int64
	Mode        string
	MD5         string
	SHA1        string
	SHA256      string
	SHA512      string
	SSDeep      string
}

func (f *fileInfo) String() string {
	return fmt.Sprintf("%s - [Created: %s, Accessed: %s, Changed: %s, Size: %v, Mode: %s] - [MD5: %s, SHA1: %s, SHA256: %s, SHA512: %s, SSDEEP: %s]",
		f.Path, f.CreatedStr, f.AccessedStr, f.ChangedStr, f.Size, f.Mode, f.MD5, f.SHA1, f.SHA256, f.SHA512, f.SSDeep)
}

func main() {
	flag.Parse()
	check()
	var list []*fileInfo
	err := filepath.Walk(*path, func(filePath string, info os.FileInfo, err error) error {
		if err != nil {
			fmt.Fprintf(os.Stderr, "Skipping %s - %v", filePath, err)
			// Just ignore the ones we have no permission to see
			return nil
		}
		skip := false
		if r != nil {
			if !r.MatchString(filePath) {
				skip = true
			}
		}
		if !skip {
			item := createItem(filePath, info)
			list = append(list, item)
			if info.IsDir() {
				if *verbose {
					fmt.Printf("%s\n", filePath)
				}
			} else if *extraVerbose {
				fmt.Println(item)
			}
		}
		return nil
	})
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error iterating %s - %v", *path, err)
	}
}

func createItem(folder string, info os.FileInfo) *fileInfo {
	item := &fileInfo{
		Changed:    info.ModTime().Unix(),
		ChangedStr: info.ModTime().String(),
		Type:       "File",
		Path:       folder,
		Size:       info.Size(),
		Mode:       info.Mode().String(),
	}
	if !info.IsDir() {
		// File type
		ext := filepath.Ext(info.Name())
		fileTypeResult := mime.TypeByExtension(ext)
		if len(fileTypeResult) == 0 && len(ext) > 0 {
			fileTypeResult = ext[1:]
		}
		if fileTypeResult != "" {
			item.Type = fileTypeResult
		}
	} else {
		item.Type = "Folder"
	}
	addOSFileInfo(item, info)
	if info.Mode().IsRegular() {
		addHashes(item.Path, item)
	}
	return item
}

type hashWrapper struct {
	hashList []hash.Hash
}

// Write ...
func (hw *hashWrapper) Write(p []byte) (n int, err error) {
	for _, hash := range hw.hashList {
		n, err = hash.Write(p)
		if err != nil {
			return
		}

		if n < len(p) {
			return 0, errors.New("Cannot write entrie file")
		}
	}
	return
}

// addHashes for (type, size, md5, sha1, sha256, sha512, spam sum...)
func addHashes(filePath string, item *fileInfo) {
	writers := &hashWrapper{
		[]hash.Hash{md5.New(), sha1.New(), sha256.New(), sha512.New()},
	}
	// Update entry file metadata
	file, err := os.Open(filePath)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Could not compute hashes for %s - %v\n", filePath, err)
		return
	}
	defer file.Close()

	_, err = io.Copy(writers, file)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Could not compute hashes for %s - %v\n", filePath, err)
		return
	}

	var md5Result []byte
	var sha1Result []byte
	var sha256Result []byte
	var sha512Result []byte

	// Spamsum (SSDeep)
	var ssdeep = ""
	sum, err := spamsum.HashReadSeeker(file, item.Size)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Could not compute SSDeep for %s - %v\n", filePath, err)
	} else {
		ssdeep = sum.String()
	}

	item.MD5 = fmt.Sprintf("%x", writers.hashList[0].Sum(md5Result))
	item.SHA1 = fmt.Sprintf("%x", writers.hashList[1].Sum(sha1Result))
	item.SHA256 = fmt.Sprintf("%x", writers.hashList[2].Sum(sha256Result))
	item.SHA512 = fmt.Sprintf("%x", writers.hashList[3].Sum(sha512Result))
	item.SSDeep = ssdeep
}
<commit_msg>added limit for files<commit_after>package main

import (
	"crypto/md5"
	"crypto/sha1"
	"crypto/sha256"
	"crypto/sha512"
	"errors"
	"flag"
	"fmt"
	"hash"
	"io"
	"mime"
	"os"
	"path/filepath"
	"regexp"

	"github.com/michielbuddingh/spamsum"
)

func message() {
	fmt.Println("Demisto uploader to create investigations and entries from directories")
	fmt.Println("======================================================================")
}

var (
	path          = flag.String("f", ".", "Folder to recursively iterage")
	username      = flag.String("u", "", "Username to login to the server")
	password      = flag.String("p", "", "Password to login to the server")
	server        = flag.String("s", "", "Demisto server URL")
	investigation = flag.String("investigation", "", "If provided, investigation ID to use instead of creating investigations")
	regex         = flag.String("regex", "", "Regex to filter files and folders. If provided, only files matching the regex will be evaluated and metadata uploaded.")
	verbose       = flag.Bool("v", true, "Verbose mode - should we print directories we are handling")
	extraVerbose  = flag.Bool("vv", false, "Very verbose - should we print details about every file")
	limit         = flag.Int("limit", -1, "Count of files we should limit ourselves to")
)

var (
	r *regexp.Regexp
)

func check() {
	/*
		if *username == "" {
			fmt.Fprintln(os.Stderr, "Please provide the username")
			os.Exit(1)
		}
		if *password == "" {
			fmt.Fprintln(os.Stderr, "Please provide the password")
			os.Exit(1)
		}
		if *server == "" {
			fmt.Fprintln(os.Stderr, "Please provide the Demisto server URL")
			os.Exit(1)
		}*/
	if *regex != "" {
		var err error
		r, err = regexp.Compile(*regex)
		if err != nil {
			fmt.Fprintf(os.Stderr, "Invalid regex - %v\n", err)
			os.Exit(1)
		}
	}
}

// fileInfo holds information about a file.
type fileInfo struct {
	Created     int64
	CreatedStr  string
	Accessed    int64
	AccessedStr string
	Changed     int64
	ChangedStr  string
	Path        string
	Type        string
	Size        int64
	Mode        string
	MD5         string
	SHA1        string
	SHA256      string
	SHA512      string
	SSDeep      string
}

func (f *fileInfo) String() string {
	return fmt.Sprintf("%s - [Created: %s, Accessed: %s, Changed: %s, Size: %v, Mode: %s] - [MD5: %s, SHA1: %s, SHA256: %s, SHA512: %s, SSDEEP: %s]",
		f.Path, f.CreatedStr, f.AccessedStr, f.ChangedStr, f.Size, f.Mode, f.MD5, f.SHA1, f.SHA256, f.SHA512, f.SSDeep)
}

func main() {
	flag.Parse()
	check()
	var list []*fileInfo
	count := 0
	err := filepath.Walk(*path, func(filePath string, info os.FileInfo, err error) error {
		if err != nil {
			fmt.Fprintf(os.Stderr, "Skipping %s - %v", filePath, err)
			// Just ignore the ones we have no permission to see
			return nil
		}
		skip := false
		if r != nil {
			if !r.MatchString(filePath) {
				skip = true
			}
		}
		if !skip {
			item := createItem(filePath, info)
			list = append(list, item)
			if info.IsDir() {
				if *verbose {
					fmt.Printf("%s\n", filePath)
				}
				// Treat every dir as separate entry
				list = []*fileInfo{}
			} else {
				if *extraVerbose {
					fmt.Println(item)
				}
				count++
				if *limit > 0 && count >= *limit {
					return fmt.Errorf("Limit of %v reached", *limit)
				}
			}
		}
		return nil
	})
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error iterating %s - %v\n", *path, err)
	}
}

func createItem(folder string, info os.FileInfo) *fileInfo {
	item := &fileInfo{
		Changed:    info.ModTime().Unix(),
		ChangedStr: info.ModTime().String(),
		Type:       "File",
		Path:       folder,
		Size:       info.Size(),
		Mode:       info.Mode().String(),
	}
	if !info.IsDir() {
		// File type
		ext := filepath.Ext(info.Name())
		fileTypeResult := mime.TypeByExtension(ext)
		if len(fileTypeResult) == 0 && len(ext) > 0 {
			fileTypeResult = ext[1:]
		}
		if fileTypeResult != "" {
			item.Type = fileTypeResult
		}
	} else {
		item.Type = "Folder"
	}
	addOSFileInfo(item, info)
	if info.Mode().IsRegular() {
		addHashes(item.Path, item)
	}
	return item
}

type hashWrapper struct {
	hashList []hash.Hash
}

// Write ...
func (hw *hashWrapper) Write(p []byte) (n int, err error) {
	for _, hash := range hw.hashList {
		n, err = hash.Write(p)
		if err != nil {
			return
		}

		if n < len(p) {
			return 0, errors.New("Cannot write entrie file")
		}
	}
	return
}

// addHashes for (type, size, md5, sha1, sha256, sha512, spam sum...)
func addHashes(filePath string, item *fileInfo) {
	writers := &hashWrapper{
		[]hash.Hash{md5.New(), sha1.New(), sha256.New(), sha512.New()},
	}
	// Update entry file metadata
	file, err := os.Open(filePath)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Could not compute hashes for %s - %v\n", filePath, err)
		return
	}
	defer file.Close()

	_, err = io.Copy(writers, file)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Could not compute hashes for %s - %v\n", filePath, err)
		return
	}

	var md5Result []byte
	var sha1Result []byte
	var sha256Result []byte
	var sha512Result []byte

	// Spamsum (SSDeep)
	var ssdeep = ""
	sum, err := spamsum.HashReadSeeker(file, item.Size)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Could not compute SSDeep for %s - %v\n", filePath, err)
	} else {
		ssdeep = sum.String()
	}

	item.MD5 = fmt.Sprintf("%x", writers.hashList[0].Sum(md5Result))
	item.SHA1 = fmt.Sprintf("%x", writers.hashList[1].Sum(sha1Result))
	item.SHA256 = fmt.Sprintf("%x", writers.hashList[2].Sum(sha256Result))
	item.SHA512 = fmt.Sprintf("%x", writers.hashList[3].Sum(sha512Result))
	item.SSDeep = ssdeep
}
<|endoftext|>


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

package build

import (
	"errors"
	"fmt"

	"github.com/r3labs/composable/yaml"
	"github.com/spf13/cobra"
)

func Generate(cmd *cobra.Command, args []string) {
	environment, _ := cmd.Flags().GetString("environment")
	overrides, _ := cmd.Flags().GetString("overrides")
	excludes, _ := cmd.Flags().GetString("excludes")
	global, _ := cmd.Flags().GetString("global-branch")
	output, _ := cmd.Flags().GetString("compose-file")
	buildpath, _ := cmd.Flags().GetString("build-path")
	edition, _ := cmd.Flags().GetString("edition")

	if buildpath == "" {
		fatal(errors.New("no build path specified"))
	}

	if len(args) < 2 {
		fatal(errors.New("generate must specify a definition and template file"))
	}

	d, err := yaml.LoadDefinition(args[0])
	if err != nil {
		fatal(err)
	}

	d.Template = args[1]
	d.BuildPath = buildpath
	d.Environment(environment)
	d.Overrides(overrides, excludes, global)

	if edition == "community" {
		for _, repo := range d.Repos {
			if repo["edition"] == "enterprise" {
				d.ExcludeRepo(repo.Name())
			}
		}
	}

	fmt.Println("cloning repos:")
	err = CloneRepos(d, buildpath)
	if err != nil {
		fatal(err)
	}

	fmt.Println("generating output")
	err = d.GenerateOutput(output)
	if err != nil {
		fatal(err)
	}
}
<commit_msg>Fix repository exclusions<commit_after>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package build

import (
	"errors"
	"fmt"

	"github.com/r3labs/composable/yaml"
	"github.com/spf13/cobra"
)

func Generate(cmd *cobra.Command, args []string) {
	environment, _ := cmd.Flags().GetString("environment")
	overrides, _ := cmd.Flags().GetString("overrides")
	excludes, _ := cmd.Flags().GetString("excludes")
	global, _ := cmd.Flags().GetString("global-branch")
	output, _ := cmd.Flags().GetString("compose-file")
	buildpath, _ := cmd.Flags().GetString("build-path")
	edition, _ := cmd.Flags().GetString("edition")

	if buildpath == "" {
		fatal(errors.New("no build path specified"))
	}

	if len(args) < 2 {
		fatal(errors.New("generate must specify a definition and template file"))
	}

	d, err := yaml.LoadDefinition(args[0])
	if err != nil {
		fatal(err)
	}

	d.Template = args[1]
	d.BuildPath = buildpath
	d.Environment(environment)
	d.Overrides(overrides, excludes, global)

	if edition == "community" {
		for i := len(d.Repos) - 1; i >= 0; i-- {
			if d.Repos[i]["edition"] == "enterprise" {
				d.ExcludeRepo(d.Repos[i].Name())
			}
		}
	}

	fmt.Println("cloning repos:")
	err = CloneRepos(d, buildpath)
	if err != nil {
		fatal(err)
	}

	fmt.Println("generating output")
	err = d.GenerateOutput(output)
	if err != nil {
		fatal(err)
	}
}
<|endoftext|>


<commit_before>package apps

import (
	"bytes"
	"fmt"
	"html/template"
	"io/ioutil"
	"net/http"
	"net/url"
	"os"
	"path"
	"strings"

	"github.com/cozy/cozy-stack/pkg/apps"
	"github.com/cozy/cozy-stack/pkg/config"
	"github.com/cozy/cozy-stack/pkg/consts"
	"github.com/cozy/cozy-stack/pkg/couchdb"
	"github.com/cozy/cozy-stack/pkg/instance"
	"github.com/cozy/cozy-stack/pkg/intents"
	"github.com/cozy/cozy-stack/pkg/sessions"
	"github.com/cozy/cozy-stack/pkg/utils"
	"github.com/cozy/cozy-stack/web/middlewares"
	"github.com/cozy/cozy-stack/web/permissions"
	"github.com/cozy/cozy-stack/web/statik"
	"github.com/labstack/echo"
)

// Serve is an handler for serving files from the VFS for a client-side app
func Serve(c echo.Context) error {
	method := c.Request().Method
	if method != "GET" && method != "HEAD" {
		return echo.NewHTTPError(http.StatusMethodNotAllowed, "Method "+method+" not allowed")
	}

	i := middlewares.GetInstance(c)
	slug := c.Get("slug").(string)

	if (!i.OnboardingFinished && slug != consts.OnboardingSlug && slug != consts.CollectSlug) ||
		(i.OnboardingFinished && slug == consts.OnboardingSlug) {
		return c.Redirect(http.StatusFound, i.PageURL("/", nil))
	}

	if config.GetConfig().Subdomains == config.FlatSubdomains {
		if code := c.QueryParam("code"); code != "" {
			return tryAuthWithSessionCode(c, i, code, slug)
		}
		if disconnect := c.QueryParam("disconnect"); disconnect == "true" || disconnect == "1" {
			return deleteAppCookie(c, i, slug)
		}
	}

	app, err := apps.GetWebappBySlug(i, slug)
	if err != nil {
		switch err {
		case apps.ErrNotFound:
			return echo.NewHTTPError(http.StatusNotFound, "Application not found")
		case apps.ErrInvalidSlugName:
			return echo.NewHTTPError(http.StatusBadRequest, err.Error())
		default:
			return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
		}
	}

	switch app.State() {
	case apps.Installed:
		return c.Redirect(http.StatusFound, i.PageURL("/auth/authorize/app", url.Values{
			"slug": {slug},
		}))
	case apps.Ready:
		return ServeAppFile(c, i, i.AppsFileServer(), app)
	default:
		return echo.NewHTTPError(http.StatusServiceUnavailable, "Application is not ready")
	}
}

// handleIntent will allow iframes from another app if the current app is
// opened as an intent
func handleIntent(c echo.Context, i *instance.Instance, slug, intentID string) {
	intent := &intents.Intent{}
	if err := couchdb.GetDoc(i, consts.Intents, intentID, intent); err != nil {
		return
	}
	allowed := false
	for _, service := range intent.Services {
		if slug == service.Slug {
			allowed = true
		}
	}
	if !allowed {
		return
	}
	parts := strings.SplitN(intent.Client, "/", 2)
	if len(parts) < 2 || parts[0] != consts.Apps {
		return
	}
	from := i.SubDomain(parts[1]).String()
	hdr := fmt.Sprintf("%s %s", middlewares.XFrameAllowFrom, from)
	c.Response().Header().Set(echo.HeaderXFrameOptions, hdr)
}

// ServeAppFile will serve the requested file using the specified application
// manifest and apps.FileServer context.
//
// It can be used to serve file application in another context than the VFS,
// for instance for tests or development puposes where we want to serve an
// application that is not installed on the user's instance. However this
// procedure should not be used for standard applications, use the Serve method
// for that.
func ServeAppFile(c echo.Context, i *instance.Instance, fs apps.FileServer, app *apps.WebappManifest) error {
	slug := app.Slug()
	route, file := app.FindRoute(path.Clean(c.Request().URL.Path))
	if route.NotFound() {
		return echo.NewHTTPError(http.StatusNotFound, "Page not found")
	}
	if file == "" {
		file = route.Index
	}

	var needAuth bool
	if len(i.RegisterToken) > 0 && file == route.Index {
		if slug != consts.OnboardingSlug || !permissions.CheckRegisterToken(c, i) {
			return c.Redirect(http.StatusFound, i.PageURL("/", nil))
		}
		needAuth = false
	} else if slug == consts.OnboardingSlug && file == route.Index {
		needAuth = true
	} else {
		needAuth = !route.Public
	}

	session, isLoggedIn := middlewares.GetSession(c)
	if needAuth && !isLoggedIn {
		if file != route.Index {
			return echo.NewHTTPError(http.StatusUnauthorized, "You must be authenticated")
		}
		reqURL := c.Request().URL
		subdomain := i.SubDomain(slug)
		subdomain.Path = reqURL.Path
		subdomain.RawQuery = reqURL.RawQuery
		subdomain.Fragment = reqURL.Fragment
		redirect := url.Values{
			"redirect": {subdomain.String()},
		}
		return c.Redirect(http.StatusFound, i.PageURL("/auth/login", redirect))
	}

	filepath := path.Join("/", route.Folder, file)
	version := app.Version()

	if file != route.Index {
		// If file is not the index, it is considered an asset of the application
		// (JS, image, ...). For theses assets we check if it contains an unique
		// identifier to help caching. In such case, a long cache (1 year) is set.
		//
		// A unique identifier is matched when the file base contains a "long"
		// hexadecimal subpart between '.', of at least 10 characters: for instance
		// "app.badf00dbadf00d.js".
		if _, id := statik.ExtractAssetID(file); id != "" {
			c.Response().Header().Set("Cache-Control", "max-age=31536000, immutable")
		}

		err := fs.ServeFileContent(c.Response(), c.Request(), slug, version, filepath)
		if os.IsNotExist(err) {
			return echo.NewHTTPError(http.StatusNotFound, "Asset not found")
		}
		if err != nil {
			return echo.NewHTTPError(http.StatusInternalServerError, err)
		}

		return nil
	}

	if intentID := c.QueryParam("intent"); intentID != "" {
		handleIntent(c, i, slug, intentID)
	}

	// For index file, we inject the locale, the stack domain, and a token if the
	// user is connected
	content, err := fs.Open(slug, version, filepath)
	if err != nil {
		return err
	}
	defer content.Close()

	buf, err := ioutil.ReadAll(content)
	if err != nil {
		return err
	}

	tmpl, err := template.New(file).Parse(string(buf))
	if err != nil {
		i.Logger().Warnf("[apps] %s cannot be parsed as a template: %s", file, err)
		return fs.ServeFileContent(c.Response(), c.Request(), slug, version, filepath)
	}

	var token string
	if isLoggedIn {
		token = i.BuildAppToken(app, session.ID())
	} else {
		token = c.QueryParam("sharecode")
	}

	tracking := "false"
	settings, err := i.SettingsDocument()
	if err == nil {
		if t, ok := settings.M["tracking"].(string); ok {
			tracking = t
		}
	}

	res := c.Response()
	res.Header().Set("Content-Type", "text/html; charset=utf-8")
	res.Header().Set("Cache-Control", "private, no-store, must-revalidate")
	res.WriteHeader(http.StatusOK)
	return tmpl.Execute(res, echo.Map{
		"Token":        token,
		"Domain":       i.Domain,
		"Locale":       i.Locale,
		"AppName":      app.Name,
		"AppEditor":    app.Editor,
		"IconPath":     app.Icon,
		"CozyBar":      cozybar(i),
		"CozyClientJS": cozyclientjs(i),
		"Tracking":     tracking,
	})
}

func tryAuthWithSessionCode(c echo.Context, i *instance.Instance, value, slug string) error {
	u := c.Request().URL
	u.Scheme = i.Scheme()
	u.Host = c.Request().Host
	if !middlewares.IsLoggedIn(c) {
		if code := sessions.FindCode(value, u.Host); code != nil {
			session, err := sessions.Get(i, code.SessionID)
			if err == nil {
				cookie, err := session.ToAppCookie(u.Host, slug)
				if err == nil {
					c.SetCookie(cookie)
				}
			}
		}
	}
	q := u.Query()
	q.Del("code")
	u.RawQuery = q.Encode()
	return c.Redirect(http.StatusFound, u.String())
}

func deleteAppCookie(c echo.Context, i *instance.Instance, slug string) error {
	c.SetCookie(&http.Cookie{
		Name:   sessions.SessionCookieName,
		Value:  "",
		MaxAge: -1,
		Path:   "/",
		Domain: utils.StripPort(i.Domain),
	})

	redirect := *(c.Request().URL)
	redirect.Scheme = i.Scheme()
	redirect.Host = c.Request().Host

	queries := make(url.Values)
	for k, v := range redirect.Query() {
		if k != "disconnect" {
			queries[k] = v
		}
	}
	redirect.RawQuery = queries.Encode()

	u := i.PageURL("/auth/login", url.Values{
		"redirect": {redirect.String()},
	})
	return c.Redirect(http.StatusFound, u)
}

var assetHelper func(domain, file string) string
var clientTemplate *template.Template
var barTemplate *template.Template

func init() {
	h := statik.NewHandler(statik.Options{Prefix: "/assets"})
	assetHelper = func(domain, file string) string {
		file = h.AssetPath(file)
		if domain != "" {
			return "//" + domain + file
		}
		return file
	}

	funcsMap := template.FuncMap{
		"split": strings.Split,
		"asset": assetHelper,
	}

	clientTemplate = template.Must(template.New("cozy-client-js").Funcs(funcsMap).Parse(`` +
		`<script defer src="{{asset .Domain "/js/cozy-client.min.js"}}"></script>`,
	))

	barTemplate = template.Must(template.New("cozy-bar").Funcs(funcsMap).Parse(`` +
		`<link rel="stylesheet" type="text/css" href="{{asset .Domain "/fonts/fonts.css"}}">` +
		`<link rel="stylesheet" type="text/css" href="{{asset .Domain "/css/cozy-bar.min.css"}}">` +
		`<script defer src="{{asset .Domain "/js/cozy-bar.min.js"}}"></script>`,
	))
}

func cozyclientjs(i *instance.Instance) template.HTML {
	buf := new(bytes.Buffer)
	err := clientTemplate.Execute(buf, echo.Map{"Domain": i.Domain})
	if err != nil {
		return template.HTML("")
	}
	return template.HTML(buf.String()) // #nosec
}

func cozybar(i *instance.Instance) template.HTML {
	buf := new(bytes.Buffer)
	err := barTemplate.Execute(buf, echo.Map{"Domain": i.Domain})
	if err != nil {
		return template.HTML("")
	}
	return template.HTML(buf.String()) // #nosec
}
<commit_msg>Override application cookie on code authentication<commit_after>package apps

import (
	"bytes"
	"fmt"
	"html/template"
	"io/ioutil"
	"net/http"
	"net/url"
	"os"
	"path"
	"strings"

	"github.com/cozy/cozy-stack/pkg/apps"
	"github.com/cozy/cozy-stack/pkg/config"
	"github.com/cozy/cozy-stack/pkg/consts"
	"github.com/cozy/cozy-stack/pkg/couchdb"
	"github.com/cozy/cozy-stack/pkg/instance"
	"github.com/cozy/cozy-stack/pkg/intents"
	"github.com/cozy/cozy-stack/pkg/sessions"
	"github.com/cozy/cozy-stack/pkg/utils"
	"github.com/cozy/cozy-stack/web/middlewares"
	"github.com/cozy/cozy-stack/web/permissions"
	"github.com/cozy/cozy-stack/web/statik"
	"github.com/labstack/echo"
)

// Serve is an handler for serving files from the VFS for a client-side app
func Serve(c echo.Context) error {
	method := c.Request().Method
	if method != "GET" && method != "HEAD" {
		return echo.NewHTTPError(http.StatusMethodNotAllowed, "Method "+method+" not allowed")
	}

	i := middlewares.GetInstance(c)
	slug := c.Get("slug").(string)

	if (!i.OnboardingFinished && slug != consts.OnboardingSlug && slug != consts.CollectSlug) ||
		(i.OnboardingFinished && slug == consts.OnboardingSlug) {
		return c.Redirect(http.StatusFound, i.PageURL("/", nil))
	}

	if config.GetConfig().Subdomains == config.FlatSubdomains {
		if code := c.QueryParam("code"); code != "" {
			return tryAuthWithSessionCode(c, i, code, slug)
		}
		if disconnect := c.QueryParam("disconnect"); disconnect == "true" || disconnect == "1" {
			return deleteAppCookie(c, i, slug)
		}
	}

	app, err := apps.GetWebappBySlug(i, slug)
	if err != nil {
		switch err {
		case apps.ErrNotFound:
			return echo.NewHTTPError(http.StatusNotFound, "Application not found")
		case apps.ErrInvalidSlugName:
			return echo.NewHTTPError(http.StatusBadRequest, err.Error())
		default:
			return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
		}
	}

	switch app.State() {
	case apps.Installed:
		return c.Redirect(http.StatusFound, i.PageURL("/auth/authorize/app", url.Values{
			"slug": {slug},
		}))
	case apps.Ready:
		return ServeAppFile(c, i, i.AppsFileServer(), app)
	default:
		return echo.NewHTTPError(http.StatusServiceUnavailable, "Application is not ready")
	}
}

// handleIntent will allow iframes from another app if the current app is
// opened as an intent
func handleIntent(c echo.Context, i *instance.Instance, slug, intentID string) {
	intent := &intents.Intent{}
	if err := couchdb.GetDoc(i, consts.Intents, intentID, intent); err != nil {
		return
	}
	allowed := false
	for _, service := range intent.Services {
		if slug == service.Slug {
			allowed = true
		}
	}
	if !allowed {
		return
	}
	parts := strings.SplitN(intent.Client, "/", 2)
	if len(parts) < 2 || parts[0] != consts.Apps {
		return
	}
	from := i.SubDomain(parts[1]).String()
	hdr := fmt.Sprintf("%s %s", middlewares.XFrameAllowFrom, from)
	c.Response().Header().Set(echo.HeaderXFrameOptions, hdr)
}

// ServeAppFile will serve the requested file using the specified application
// manifest and apps.FileServer context.
//
// It can be used to serve file application in another context than the VFS,
// for instance for tests or development puposes where we want to serve an
// application that is not installed on the user's instance. However this
// procedure should not be used for standard applications, use the Serve method
// for that.
func ServeAppFile(c echo.Context, i *instance.Instance, fs apps.FileServer, app *apps.WebappManifest) error {
	slug := app.Slug()
	route, file := app.FindRoute(path.Clean(c.Request().URL.Path))
	if route.NotFound() {
		return echo.NewHTTPError(http.StatusNotFound, "Page not found")
	}
	if file == "" {
		file = route.Index
	}

	var needAuth bool
	if len(i.RegisterToken) > 0 && file == route.Index {
		if slug != consts.OnboardingSlug || !permissions.CheckRegisterToken(c, i) {
			return c.Redirect(http.StatusFound, i.PageURL("/", nil))
		}
		needAuth = false
	} else if slug == consts.OnboardingSlug && file == route.Index {
		needAuth = true
	} else {
		needAuth = !route.Public
	}

	session, isLoggedIn := middlewares.GetSession(c)
	if needAuth && !isLoggedIn {
		if file != route.Index {
			return echo.NewHTTPError(http.StatusUnauthorized, "You must be authenticated")
		}
		reqURL := c.Request().URL
		subdomain := i.SubDomain(slug)
		subdomain.Path = reqURL.Path
		subdomain.RawQuery = reqURL.RawQuery
		subdomain.Fragment = reqURL.Fragment
		redirect := url.Values{
			"redirect": {subdomain.String()},
		}
		return c.Redirect(http.StatusFound, i.PageURL("/auth/login", redirect))
	}

	filepath := path.Join("/", route.Folder, file)
	version := app.Version()

	if file != route.Index {
		// If file is not the index, it is considered an asset of the application
		// (JS, image, ...). For theses assets we check if it contains an unique
		// identifier to help caching. In such case, a long cache (1 year) is set.
		//
		// A unique identifier is matched when the file base contains a "long"
		// hexadecimal subpart between '.', of at least 10 characters: for instance
		// "app.badf00dbadf00d.js".
		if _, id := statik.ExtractAssetID(file); id != "" {
			c.Response().Header().Set("Cache-Control", "max-age=31536000, immutable")
		}

		err := fs.ServeFileContent(c.Response(), c.Request(), slug, version, filepath)
		if os.IsNotExist(err) {
			return echo.NewHTTPError(http.StatusNotFound, "Asset not found")
		}
		if err != nil {
			return echo.NewHTTPError(http.StatusInternalServerError, err)
		}

		return nil
	}

	if intentID := c.QueryParam("intent"); intentID != "" {
		handleIntent(c, i, slug, intentID)
	}

	// For index file, we inject the locale, the stack domain, and a token if the
	// user is connected
	content, err := fs.Open(slug, version, filepath)
	if err != nil {
		return err
	}
	defer content.Close()

	buf, err := ioutil.ReadAll(content)
	if err != nil {
		return err
	}

	tmpl, err := template.New(file).Parse(string(buf))
	if err != nil {
		i.Logger().Warnf("[apps] %s cannot be parsed as a template: %s", file, err)
		return fs.ServeFileContent(c.Response(), c.Request(), slug, version, filepath)
	}

	var token string
	if isLoggedIn {
		token = i.BuildAppToken(app, session.ID())
	} else {
		token = c.QueryParam("sharecode")
	}

	tracking := "false"
	settings, err := i.SettingsDocument()
	if err == nil {
		if t, ok := settings.M["tracking"].(string); ok {
			tracking = t
		}
	}

	res := c.Response()
	res.Header().Set("Content-Type", "text/html; charset=utf-8")
	res.Header().Set("Cache-Control", "private, no-store, must-revalidate")
	res.WriteHeader(http.StatusOK)
	return tmpl.Execute(res, echo.Map{
		"Token":        token,
		"Domain":       i.Domain,
		"Locale":       i.Locale,
		"AppName":      app.Name,
		"AppEditor":    app.Editor,
		"IconPath":     app.Icon,
		"CozyBar":      cozybar(i),
		"CozyClientJS": cozyclientjs(i),
		"Tracking":     tracking,
	})
}

func tryAuthWithSessionCode(c echo.Context, i *instance.Instance, value, slug string) error {
	u := *(c.Request().URL)
	u.Scheme = i.Scheme()
	u.Host = c.Request().Host
	if code := sessions.FindCode(value, u.Host); code != nil {
		session, err := sessions.Get(i, code.SessionID)
		if err == nil {
			cookie, err := session.ToAppCookie(u.Host, slug)
			if err == nil {
				c.SetCookie(cookie)
			}
		}
	}
	q := u.Query()
	q.Del("code")
	u.RawQuery = q.Encode()
	return c.Redirect(http.StatusFound, u.String())
}

func deleteAppCookie(c echo.Context, i *instance.Instance, slug string) error {
	c.SetCookie(&http.Cookie{
		Name:   sessions.SessionCookieName,
		Value:  "",
		MaxAge: -1,
		Path:   "/",
		Domain: utils.StripPort(i.Domain),
	})

	redirect := *(c.Request().URL)
	redirect.Scheme = i.Scheme()
	redirect.Host = c.Request().Host

	queries := make(url.Values)
	for k, v := range redirect.Query() {
		if k != "disconnect" {
			queries[k] = v
		}
	}
	redirect.RawQuery = queries.Encode()

	u := i.PageURL("/auth/login", url.Values{
		"redirect": {redirect.String()},
	})
	return c.Redirect(http.StatusFound, u)
}

var assetHelper func(domain, file string) string
var clientTemplate *template.Template
var barTemplate *template.Template

func init() {
	h := statik.NewHandler(statik.Options{Prefix: "/assets"})
	assetHelper = func(domain, file string) string {
		file = h.AssetPath(file)
		if domain != "" {
			return "//" + domain + file
		}
		return file
	}

	funcsMap := template.FuncMap{
		"split": strings.Split,
		"asset": assetHelper,
	}

	clientTemplate = template.Must(template.New("cozy-client-js").Funcs(funcsMap).Parse(`` +
		`<script defer src="{{asset .Domain "/js/cozy-client.min.js"}}"></script>`,
	))

	barTemplate = template.Must(template.New("cozy-bar").Funcs(funcsMap).Parse(`` +
		`<link rel="stylesheet" type="text/css" href="{{asset .Domain "/fonts/fonts.css"}}">` +
		`<link rel="stylesheet" type="text/css" href="{{asset .Domain "/css/cozy-bar.min.css"}}">` +
		`<script defer src="{{asset .Domain "/js/cozy-bar.min.js"}}"></script>`,
	))
}

func cozyclientjs(i *instance.Instance) template.HTML {
	buf := new(bytes.Buffer)
	err := clientTemplate.Execute(buf, echo.Map{"Domain": i.Domain})
	if err != nil {
		return template.HTML("")
	}
	return template.HTML(buf.String()) // #nosec
}

func cozybar(i *instance.Instance) template.HTML {
	buf := new(bytes.Buffer)
	err := barTemplate.Execute(buf, echo.Map{"Domain": i.Domain})
	if err != nil {
		return template.HTML("")
	}
	return template.HTML(buf.String()) // #nosec
}
<|endoftext|>


<commit_before>package vault

import (
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"os"
	"time"

	"github.com/99designs/aws-vault/prompt"
	"github.com/99designs/keyring"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/sts"
)

const (
	MaxSessionDuration    = time.Hour * 36
	MinSessionDuration    = time.Minute * 15
	MinAssumeRoleDuration = time.Minute * 15
	MaxAssumeRoleDuration = time.Hour

	DefaultSessionDuration    = time.Hour * 4
	DefaultAssumeRoleDuration = time.Minute * 15
)

type VaultOptions struct {
	SessionDuration    time.Duration
	AssumeRoleDuration time.Duration
	ExpiryWindow       time.Duration
	MfaToken           string
	MfaPrompt          prompt.PromptFunc
	NoSession          bool
	Config             *Config
	MasterCreds        *credentials.Value
}

func (o VaultOptions) Validate() error {
	if o.SessionDuration < MinSessionDuration {
		return errors.New("Minimum session duration is " + MinSessionDuration.String())
	} else if o.SessionDuration > MaxSessionDuration {
		return errors.New("Maximum session duration is " + MaxSessionDuration.String())
	}
	if o.AssumeRoleDuration < MinAssumeRoleDuration {
		return errors.New("Minimum duration for assumed roles is " + MinAssumeRoleDuration.String())
	} else if o.AssumeRoleDuration > MaxAssumeRoleDuration {
		return errors.New("Maximum duration for assumed roles is " + MaxAssumeRoleDuration.String())
	}

	return nil
}

func (o VaultOptions) ApplyDefaults() VaultOptions {
	if o.AssumeRoleDuration == 0 {
		o.AssumeRoleDuration = DefaultAssumeRoleDuration
	}
	if o.SessionDuration == 0 {
		o.SessionDuration = DefaultSessionDuration
	}
	return o
}

type VaultProvider struct {
	credentials.Expiry
	VaultOptions
	profile  string
	expires  time.Time
	keyring  keyring.Keyring
	sessions *KeyringSessions
	config   *Config
	creds    map[string]credentials.Value
}

func NewVaultProvider(k keyring.Keyring, profile string, opts VaultOptions) (*VaultProvider, error) {
	opts = opts.ApplyDefaults()
	if err := opts.Validate(); err != nil {
		return nil, err
	}
	return &VaultProvider{
		VaultOptions: opts,
		keyring:      k,
		sessions:     &KeyringSessions{k, opts.Config},
		profile:      profile,
		config:       opts.Config,
		creds:        map[string]credentials.Value{},
	}, nil
}

// Retrieve returns credentials protected by a GetSessionToken. If there is an associated
// role in the profile then AssumeRole is applied. The benefit of a session is that it doesn't
// require MFA or a user prompt to access the keychain item, much like sudo.
func (p *VaultProvider) Retrieve() (credentials.Value, error) {
	if p.NoSession {
		return p.RetrieveWithoutSessionToken()
	}

	session, err := p.sessions.Retrieve(p.profile, p.SessionDuration)
	if err != nil {
		if err == keyring.ErrKeyNotFound {
			log.Println("Session not found in keyring")
		} else {
			log.Println(err)
		}

		// session lookup missed, we need to create a new one
		creds, err := p.getMasterCreds()
		if err != nil {
			return credentials.Value{}, err
		}

		session, err = p.getSessionToken(&creds)
		if err != nil {
			return credentials.Value{}, err
		}

		if err = p.sessions.Store(p.profile, session, p.SessionDuration); err != nil {
			return credentials.Value{}, err
		}
	}

	log.Printf("Using session ****************%s, expires in %s",
		(*session.AccessKeyId)[len(*session.AccessKeyId)-4:],
		session.Expiration.Sub(time.Now()).String())

	if profile, exists := p.config.Profile(p.profile); exists && profile.RoleARN != "" {
		session, err = p.assumeRoleFromSession(session, profile.RoleARN)
		if err != nil {
			return credentials.Value{}, err
		}

		log.Printf("Using role ****************%s (from session token), expires in %s",
			(*session.AccessKeyId)[len(*session.AccessKeyId)-4:],
			session.Expiration.Sub(time.Now()).String())
	}

	window := p.ExpiryWindow
	if window == 0 {
		window = time.Minute * 5
	}

	p.SetExpiration(*session.Expiration, window)
	p.expires = *session.Expiration

	value := credentials.Value{
		AccessKeyID:     *session.AccessKeyId,
		SecretAccessKey: *session.SecretAccessKey,
		SessionToken:    *session.SessionToken,
	}

	return value, nil
}

// RetrieveWithoutSessionToken returns credentials that are either the master credentials or
// a session created with AssumeRole. This allows for usecases where a token created with AssumeRole
// wouldn't work.
func (p *VaultProvider) RetrieveWithoutSessionToken() (credentials.Value, error) {
	log.Println("Skipping session token and using master credentials directly")

	creds, err := p.getMasterCreds()
	if err != nil {
		return credentials.Value{}, err
	}

	if profile, exists := p.config.Profile(p.profile); exists && profile.RoleARN != "" {
		session, err := p.assumeRole(creds, profile.RoleARN)
		if err != nil {
			return credentials.Value{}, err
		}

		log.Printf("Using role ****************%s, expires in %s",
			(*session.AccessKeyId)[len(*session.AccessKeyId)-4:],
			session.Expiration.Sub(time.Now()).String())

		window := p.ExpiryWindow
		if window == 0 {
			window = time.Minute * 5
		}

		p.SetExpiration(*session.Expiration, window)
		p.expires = *session.Expiration

		value := credentials.Value{
			AccessKeyID:     *session.AccessKeyId,
			SecretAccessKey: *session.SecretAccessKey,
			SessionToken:    *session.SessionToken,
		}

		return value, nil
	}

	// no role, exposes master credentials which don't expire
	return creds, nil
}

func (p VaultProvider) awsConfig() *aws.Config {
	if region := os.Getenv("AWS_REGION"); region != "" {
		log.Printf("Using region %q from AWS_REGION", region)
		return aws.NewConfig().WithRegion(region)
	}

	if region := os.Getenv("AWS_DEFAULT_REGION"); region != "" {
		log.Printf("Using region %q from AWS_DEFAULT_REGION", region)
		return aws.NewConfig().WithRegion(region)
	}

	if profile, ok := p.profiles[p.profile]; ok {
		if region, hasRegion := profile["region"]; hasRegion {
			log.Printf("Using region %q from profile", region)
			return aws.NewConfig().WithRegion(region)
		}
	}

	return aws.NewConfig()
}

func (p *VaultProvider) getMasterCreds() (credentials.Value, error) {
	if p.MasterCreds != nil {
		return *p.MasterCreds, nil
	}

	source, _ := p.Config.SourceProfile(p.profile)

	val, ok := p.creds[source.Name]
	if !ok {
		creds := credentials.NewCredentials(&KeyringProvider{Keyring: p.keyring, Profile: source.Name})

		var err error
		if val, err = creds.Get(); err != nil {
			log.Printf("Failed to find credentials for profile %q in keyring", source.Name)
			return val, err
		}

		p.creds[source.Name] = val
	}

	return val, nil
}

func (p *VaultProvider) getSessionToken(creds *credentials.Value) (sts.Credentials, error) {
	params := &sts.GetSessionTokenInput{
		DurationSeconds: aws.Int64(int64(p.SessionDuration.Seconds())),
	}

	if profile, _ := p.Config.Profile(p.profile); profile.MFASerial != "" {
		params.SerialNumber = aws.String(profile.MFASerial)
		if p.MfaToken == "" {
			token, err := p.MfaPrompt(fmt.Sprintf("Enter token for %s: ", profile.MFASerial))
			if err != nil {
				return sts.Credentials{}, err
			}
			params.TokenCode = aws.String(token)
		} else {
			params.TokenCode = aws.String(p.MfaToken)
		}
	}

	client := sts.New(session.New(p.awsConfig().
		WithCredentials(credentials.NewCredentials(&credentials.StaticProvider{
			Value: *creds,
		}))))

	source, _ := p.Config.SourceProfile(p.profile)
	log.Printf("Getting new session token for profile %s", source.Name)

	resp, err := client.GetSessionToken(params)
	if err != nil {
		return sts.Credentials{}, err
	}

	return *resp.Credentials, nil
}

func (p *VaultProvider) roleSessionName() string {
	if profile, _ := p.Config.Profile(p.profile); profile.RoleSessionName != "" {
		return profile.RoleSessionName
	}

	// Try to work out a role name that will hopefully end up unique.
	return fmt.Sprintf("%d", time.Now().UTC().UnixNano())
}

// assumeRoleFromSession takes a session created with GetSessionToken and uses that to assume a role
func (p *VaultProvider) assumeRoleFromSession(creds sts.Credentials, roleArn string) (sts.Credentials, error) {
	client := sts.New(session.New(p.awsConfig().
		WithCredentials(credentials.NewStaticCredentials(
			*creds.AccessKeyId,
			*creds.SecretAccessKey,
			*creds.SessionToken,
		))))

	input := &sts.AssumeRoleInput{
		RoleArn:         aws.String(roleArn),
		RoleSessionName: aws.String(p.roleSessionName()),
		DurationSeconds: aws.Int64(int64(p.AssumeRoleDuration.Seconds())),
	}

	log.Printf("Assuming role %s from session token", roleArn)
	resp, err := client.AssumeRole(input)
	if err != nil {
		return sts.Credentials{}, err
	}

	return *resp.Credentials, nil
}

// assumeRole uses IAM credentials to assume a role
func (p *VaultProvider) assumeRole(creds credentials.Value, roleArn string) (sts.Credentials, error) {
	client := sts.New(session.New(p.awsConfig().
		WithCredentials(credentials.NewCredentials(&credentials.StaticProvider{Value: creds})),
	))

	input := &sts.AssumeRoleInput{
		RoleArn:         aws.String(roleArn),
		RoleSessionName: aws.String(p.roleSessionName()),
		DurationSeconds: aws.Int64(int64(p.AssumeRoleDuration.Seconds())),
	}

	// if we don't have a session, we need to include MFA token in the AssumeRole call
	if profile, _ := p.Config.Profile(p.profile); profile.MFASerial != "" {
		input.SerialNumber = aws.String(profile.MFASerial)
		if p.MfaToken == "" {
			token, err := p.MfaPrompt(fmt.Sprintf("Enter token for %s: ", profile.MFASerial))
			if err != nil {
				return sts.Credentials{}, err
			}
			input.TokenCode = aws.String(token)
		} else {
			input.TokenCode = aws.String(p.MfaToken)
		}
	}

	log.Printf("Assuming role %s with iam credentials", roleArn)
	resp, err := client.AssumeRole(input)
	if err != nil {
		return sts.Credentials{}, err
	}

	return *resp.Credentials, nil
}

type KeyringProvider struct {
	Keyring keyring.Keyring
	Profile string
}

func (p *KeyringProvider) IsExpired() bool {
	return false
}

func (p *KeyringProvider) Retrieve() (val credentials.Value, err error) {
	log.Printf("Looking up keyring for %s", p.Profile)
	item, err := p.Keyring.Get(p.Profile)
	if err != nil {
		log.Println("Error from keyring", err)
		return val, err
	}
	if err = json.Unmarshal(item.Data, &val); err != nil {
		return val, fmt.Errorf("Invalid data in keyring: %v", err)
	}
	return val, err
}

func (p *KeyringProvider) Store(val credentials.Value) error {
	bytes, err := json.Marshal(val)
	if err != nil {
		return err
	}

	return p.Keyring.Set(keyring.Item{
		Key:  p.Profile,
		Data: bytes,
	})
}

func (p *KeyringProvider) Delete() error {
	return p.Keyring.Remove(p.Profile)
}

type VaultCredentials struct {
	*credentials.Credentials
	provider *VaultProvider
}

func NewVaultCredentials(k keyring.Keyring, profile string, opts VaultOptions) (*VaultCredentials, error) {
	provider, err := NewVaultProvider(k, profile, opts)
	if err != nil {
		return nil, err
	}

	return &VaultCredentials{credentials.NewCredentials(provider), provider}, nil
}

func (v *VaultCredentials) Expires() time.Time {
	return v.provider.expires
}
<commit_msg>Fix bad merge<commit_after>package vault

import (
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"os"
	"time"

	"github.com/99designs/aws-vault/prompt"
	"github.com/99designs/keyring"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/sts"
)

const (
	MaxSessionDuration    = time.Hour * 36
	MinSessionDuration    = time.Minute * 15
	MinAssumeRoleDuration = time.Minute * 15
	MaxAssumeRoleDuration = time.Hour

	DefaultSessionDuration    = time.Hour * 4
	DefaultAssumeRoleDuration = time.Minute * 15
)

type VaultOptions struct {
	SessionDuration    time.Duration
	AssumeRoleDuration time.Duration
	ExpiryWindow       time.Duration
	MfaToken           string
	MfaPrompt          prompt.PromptFunc
	NoSession          bool
	Config             *Config
	MasterCreds        *credentials.Value
}

func (o VaultOptions) Validate() error {
	if o.SessionDuration < MinSessionDuration {
		return errors.New("Minimum session duration is " + MinSessionDuration.String())
	} else if o.SessionDuration > MaxSessionDuration {
		return errors.New("Maximum session duration is " + MaxSessionDuration.String())
	}
	if o.AssumeRoleDuration < MinAssumeRoleDuration {
		return errors.New("Minimum duration for assumed roles is " + MinAssumeRoleDuration.String())
	} else if o.AssumeRoleDuration > MaxAssumeRoleDuration {
		return errors.New("Maximum duration for assumed roles is " + MaxAssumeRoleDuration.String())
	}

	return nil
}

func (o VaultOptions) ApplyDefaults() VaultOptions {
	if o.AssumeRoleDuration == 0 {
		o.AssumeRoleDuration = DefaultAssumeRoleDuration
	}
	if o.SessionDuration == 0 {
		o.SessionDuration = DefaultSessionDuration
	}
	return o
}

type VaultProvider struct {
	credentials.Expiry
	VaultOptions
	profile  string
	expires  time.Time
	keyring  keyring.Keyring
	sessions *KeyringSessions
	config   *Config
	creds    map[string]credentials.Value
}

func NewVaultProvider(k keyring.Keyring, profile string, opts VaultOptions) (*VaultProvider, error) {
	opts = opts.ApplyDefaults()
	if err := opts.Validate(); err != nil {
		return nil, err
	}
	return &VaultProvider{
		VaultOptions: opts,
		keyring:      k,
		sessions:     &KeyringSessions{k, opts.Config},
		profile:      profile,
		config:       opts.Config,
		creds:        map[string]credentials.Value{},
	}, nil
}

// Retrieve returns credentials protected by a GetSessionToken. If there is an associated
// role in the profile then AssumeRole is applied. The benefit of a session is that it doesn't
// require MFA or a user prompt to access the keychain item, much like sudo.
func (p *VaultProvider) Retrieve() (credentials.Value, error) {
	if p.NoSession {
		return p.RetrieveWithoutSessionToken()
	}

	session, err := p.sessions.Retrieve(p.profile, p.SessionDuration)
	if err != nil {
		if err == keyring.ErrKeyNotFound {
			log.Println("Session not found in keyring")
		} else {
			log.Println(err)
		}

		// session lookup missed, we need to create a new one
		creds, err := p.getMasterCreds()
		if err != nil {
			return credentials.Value{}, err
		}

		session, err = p.getSessionToken(&creds)
		if err != nil {
			return credentials.Value{}, err
		}

		if err = p.sessions.Store(p.profile, session, p.SessionDuration); err != nil {
			return credentials.Value{}, err
		}
	}

	log.Printf("Using session ****************%s, expires in %s",
		(*session.AccessKeyId)[len(*session.AccessKeyId)-4:],
		session.Expiration.Sub(time.Now()).String())

	if profile, exists := p.config.Profile(p.profile); exists && profile.RoleARN != "" {
		session, err = p.assumeRoleFromSession(session, profile.RoleARN)
		if err != nil {
			return credentials.Value{}, err
		}

		log.Printf("Using role ****************%s (from session token), expires in %s",
			(*session.AccessKeyId)[len(*session.AccessKeyId)-4:],
			session.Expiration.Sub(time.Now()).String())
	}

	window := p.ExpiryWindow
	if window == 0 {
		window = time.Minute * 5
	}

	p.SetExpiration(*session.Expiration, window)
	p.expires = *session.Expiration

	value := credentials.Value{
		AccessKeyID:     *session.AccessKeyId,
		SecretAccessKey: *session.SecretAccessKey,
		SessionToken:    *session.SessionToken,
	}

	return value, nil
}

// RetrieveWithoutSessionToken returns credentials that are either the master credentials or
// a session created with AssumeRole. This allows for usecases where a token created with AssumeRole
// wouldn't work.
func (p *VaultProvider) RetrieveWithoutSessionToken() (credentials.Value, error) {
	log.Println("Skipping session token and using master credentials directly")

	creds, err := p.getMasterCreds()
	if err != nil {
		return credentials.Value{}, err
	}

	if profile, exists := p.config.Profile(p.profile); exists && profile.RoleARN != "" {
		session, err := p.assumeRole(creds, profile.RoleARN)
		if err != nil {
			return credentials.Value{}, err
		}

		log.Printf("Using role ****************%s, expires in %s",
			(*session.AccessKeyId)[len(*session.AccessKeyId)-4:],
			session.Expiration.Sub(time.Now()).String())

		window := p.ExpiryWindow
		if window == 0 {
			window = time.Minute * 5
		}

		p.SetExpiration(*session.Expiration, window)
		p.expires = *session.Expiration

		value := credentials.Value{
			AccessKeyID:     *session.AccessKeyId,
			SecretAccessKey: *session.SecretAccessKey,
			SessionToken:    *session.SessionToken,
		}

		return value, nil
	}

	// no role, exposes master credentials which don't expire
	return creds, nil
}

func (p VaultProvider) awsConfig() *aws.Config {
	if region := os.Getenv("AWS_REGION"); region != "" {
		log.Printf("Using region %q from AWS_REGION", region)
		return aws.NewConfig().WithRegion(region)
	}

	if region := os.Getenv("AWS_DEFAULT_REGION"); region != "" {
		log.Printf("Using region %q from AWS_DEFAULT_REGION", region)
		return aws.NewConfig().WithRegion(region)
	}

	if profile, ok := p.Config.Profile(p.profile); ok {
		if profile.Region != "" {
			log.Printf("Using region %q from profile", profile.Region)
			return aws.NewConfig().WithRegion(profile.Region)
		}
	}

	return aws.NewConfig()
}

func (p *VaultProvider) getMasterCreds() (credentials.Value, error) {
	if p.MasterCreds != nil {
		return *p.MasterCreds, nil
	}

	source, _ := p.Config.SourceProfile(p.profile)

	val, ok := p.creds[source.Name]
	if !ok {
		creds := credentials.NewCredentials(&KeyringProvider{Keyring: p.keyring, Profile: source.Name})

		var err error
		if val, err = creds.Get(); err != nil {
			log.Printf("Failed to find credentials for profile %q in keyring", source.Name)
			return val, err
		}

		p.creds[source.Name] = val
	}

	return val, nil
}

func (p *VaultProvider) getSessionToken(creds *credentials.Value) (sts.Credentials, error) {
	params := &sts.GetSessionTokenInput{
		DurationSeconds: aws.Int64(int64(p.SessionDuration.Seconds())),
	}

	if profile, _ := p.Config.Profile(p.profile); profile.MFASerial != "" {
		params.SerialNumber = aws.String(profile.MFASerial)
		if p.MfaToken == "" {
			token, err := p.MfaPrompt(fmt.Sprintf("Enter token for %s: ", profile.MFASerial))
			if err != nil {
				return sts.Credentials{}, err
			}
			params.TokenCode = aws.String(token)
		} else {
			params.TokenCode = aws.String(p.MfaToken)
		}
	}

	client := sts.New(session.New(p.awsConfig().
		WithCredentials(credentials.NewCredentials(&credentials.StaticProvider{
			Value: *creds,
		}))))

	source, _ := p.Config.SourceProfile(p.profile)
	log.Printf("Getting new session token for profile %s", source.Name)

	resp, err := client.GetSessionToken(params)
	if err != nil {
		return sts.Credentials{}, err
	}

	return *resp.Credentials, nil
}

func (p *VaultProvider) roleSessionName() string {
	if profile, _ := p.Config.Profile(p.profile); profile.RoleSessionName != "" {
		return profile.RoleSessionName
	}

	// Try to work out a role name that will hopefully end up unique.
	return fmt.Sprintf("%d", time.Now().UTC().UnixNano())
}

// assumeRoleFromSession takes a session created with GetSessionToken and uses that to assume a role
func (p *VaultProvider) assumeRoleFromSession(creds sts.Credentials, roleArn string) (sts.Credentials, error) {
	client := sts.New(session.New(p.awsConfig().
		WithCredentials(credentials.NewStaticCredentials(
			*creds.AccessKeyId,
			*creds.SecretAccessKey,
			*creds.SessionToken,
		))))

	input := &sts.AssumeRoleInput{
		RoleArn:         aws.String(roleArn),
		RoleSessionName: aws.String(p.roleSessionName()),
		DurationSeconds: aws.Int64(int64(p.AssumeRoleDuration.Seconds())),
	}

	log.Printf("Assuming role %s from session token", roleArn)
	resp, err := client.AssumeRole(input)
	if err != nil {
		return sts.Credentials{}, err
	}

	return *resp.Credentials, nil
}

// assumeRole uses IAM credentials to assume a role
func (p *VaultProvider) assumeRole(creds credentials.Value, roleArn string) (sts.Credentials, error) {
	client := sts.New(session.New(p.awsConfig().
		WithCredentials(credentials.NewCredentials(&credentials.StaticProvider{Value: creds})),
	))

	input := &sts.AssumeRoleInput{
		RoleArn:         aws.String(roleArn),
		RoleSessionName: aws.String(p.roleSessionName()),
		DurationSeconds: aws.Int64(int64(p.AssumeRoleDuration.Seconds())),
	}

	// if we don't have a session, we need to include MFA token in the AssumeRole call
	if profile, _ := p.Config.Profile(p.profile); profile.MFASerial != "" {
		input.SerialNumber = aws.String(profile.MFASerial)
		if p.MfaToken == "" {
			token, err := p.MfaPrompt(fmt.Sprintf("Enter token for %s: ", profile.MFASerial))
			if err != nil {
				return sts.Credentials{}, err
			}
			input.TokenCode = aws.String(token)
		} else {
			input.TokenCode = aws.String(p.MfaToken)
		}
	}

	log.Printf("Assuming role %s with iam credentials", roleArn)
	resp, err := client.AssumeRole(input)
	if err != nil {
		return sts.Credentials{}, err
	}

	return *resp.Credentials, nil
}

type KeyringProvider struct {
	Keyring keyring.Keyring
	Profile string
}

func (p *KeyringProvider) IsExpired() bool {
	return false
}

func (p *KeyringProvider) Retrieve() (val credentials.Value, err error) {
	log.Printf("Looking up keyring for %s", p.Profile)
	item, err := p.Keyring.Get(p.Profile)
	if err != nil {
		log.Println("Error from keyring", err)
		return val, err
	}
	if err = json.Unmarshal(item.Data, &val); err != nil {
		return val, fmt.Errorf("Invalid data in keyring: %v", err)
	}
	return val, err
}

func (p *KeyringProvider) Store(val credentials.Value) error {
	bytes, err := json.Marshal(val)
	if err != nil {
		return err
	}

	return p.Keyring.Set(keyring.Item{
		Key:  p.Profile,
		Data: bytes,
	})
}

func (p *KeyringProvider) Delete() error {
	return p.Keyring.Remove(p.Profile)
}

type VaultCredentials struct {
	*credentials.Credentials
	provider *VaultProvider
}

func NewVaultCredentials(k keyring.Keyring, profile string, opts VaultOptions) (*VaultCredentials, error) {
	provider, err := NewVaultProvider(k, profile, opts)
	if err != nil {
		return nil, err
	}

	return &VaultCredentials{credentials.NewCredentials(provider), provider}, nil
}

func (v *VaultCredentials) Expires() time.Time {
	return v.provider.expires
}
<|endoftext|>


<commit_before>package vcsclient

import (
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"reflect"
	"strconv"
	"strings"

	"github.com/google/go-querystring/query"
	muxpkg "github.com/sourcegraph/mux"
	"golang.org/x/tools/godoc/vfs"
	"sourcegraph.com/sourcegraph/go-vcs/vcs"
)

var ErrRepoNotExist = errors.New("repository does not exist on remote server")

func IsRepoNotExist(err error) bool {
	if err == nil {
		return false
	}
	if err == ErrRepoNotExist {
		return true
	}
	if err, ok := err.(*ErrorResponse); ok {
		return err.Message == ErrRepoNotExist.Error()
	}
	return err.Error() == ErrRepoNotExist.Error()
}

type repository struct {
	client   *Client
	repoPath string
}

var _ vcs.Repository = (*repository)(nil)

type RepositoryCloneUpdater interface {
	// CloneOrUpdate instructs the server to clone the repository so
	// it is available to the client via the API if it doesn't yet
	// exist, or update it from its default remote. The call blocks
	// until cloning finishes or fails.
	CloneOrUpdate(cloneInfo *CloneInfo) error
}

// CloneInfo is the information needed to clone a repository.
type CloneInfo struct {
	// VCS is the type of VCS (e.g., "git")
	VCS string

	// CloneURL is the remote URL from which to clone.
	CloneURL string

	// Additional options
	vcs.RemoteOpts
}

func (r *repository) CloneOrUpdate(cloneInfo *CloneInfo) error {
	url, err := r.url(RouteRepo, nil, nil)
	if err != nil {
		return err
	}

	req, err := r.client.NewRequest("POST", url.String(), cloneInfo)
	if err != nil {
		return err
	}

	resp, err := r.client.Do(req, nil)
	if err != nil {
		return err
	}
	if c := resp.StatusCode; c != http.StatusOK && c != http.StatusCreated {
		return fmt.Errorf("CloneOrUpdate: HTTP error %d", c)
	}

	return nil
}

func (r *repository) ResolveBranch(name string) (vcs.CommitID, error) {
	url, err := r.url(RouteRepoBranch, map[string]string{"Branch": name}, nil)
	if err != nil {
		return "", err
	}

	req, err := r.client.NewRequest("GET", url.String(), nil)
	if err != nil {
		return "", err
	}

	resp, err := r.client.doIgnoringRedirects(req)
	if err != nil {
		return "", err
	}

	return r.parseCommitIDInURL(resp.Header.Get("location"))
}

func (r *repository) ResolveRevision(spec string) (vcs.CommitID, error) {
	url, err := r.url(RouteRepoRevision, map[string]string{"RevSpec": spec}, nil)
	if err != nil {
		return "", err
	}

	req, err := r.client.NewRequest("GET", url.String(), nil)
	if err != nil {
		return "", err
	}

	resp, err := r.client.doIgnoringRedirects(req)
	if err != nil {
		return "", err
	}

	return r.parseCommitIDInURL(resp.Header.Get("location"))
}

func (r *repository) ResolveTag(name string) (vcs.CommitID, error) {
	url, err := r.url(RouteRepoTag, map[string]string{"Tag": name}, nil)
	if err != nil {
		return "", err
	}

	req, err := r.client.NewRequest("GET", url.String(), nil)
	if err != nil {
		return "", err
	}

	resp, err := r.client.doIgnoringRedirects(req)
	if err != nil {
		return "", err
	}

	return r.parseCommitIDInURL(resp.Header.Get("location"))
}

func (r *repository) parseCommitIDInURL(urlStr string) (vcs.CommitID, error) {
	url, err := url.Parse(urlStr)
	if err != nil {
		return "", err
	}

	var info muxpkg.RouteMatch
	match := (*muxpkg.Router)(router).Match(&http.Request{Method: "GET", URL: url}, &info)
	if !match || info.Vars["CommitID"] == "" {
		return "", errors.New("failed to determine CommitID from URL")
	}

	return vcs.CommitID(info.Vars["CommitID"]), nil
}

func (r *repository) Branches(opt vcs.BranchesOptions) ([]*vcs.Branch, error) {
	url, err := r.url(RouteRepoBranches, nil, opt)
	if err != nil {
		return nil, err
	}

	req, err := r.client.NewRequest("GET", url.String(), nil)
	if err != nil {
		return nil, err
	}

	var branches []*vcs.Branch
	_, err = r.client.Do(req, &branches)
	if err != nil {
		return nil, err
	}

	return branches, nil
}

func (r *repository) Tags() ([]*vcs.Tag, error) {
	url, err := r.url(RouteRepoTags, nil, nil)
	if err != nil {
		return nil, err
	}

	req, err := r.client.NewRequest("GET", url.String(), nil)
	if err != nil {
		return nil, err
	}

	var tags []*vcs.Tag
	_, err = r.client.Do(req, &tags)
	if err != nil {
		return nil, err
	}

	return tags, nil
}

func (r *repository) GetCommit(id vcs.CommitID) (*vcs.Commit, error) {
	url, err := r.url(RouteRepoCommit, map[string]string{"CommitID": string(id)}, nil)
	if err != nil {
		return nil, err
	}

	req, err := r.client.NewRequest("GET", url.String(), nil)
	if err != nil {
		return nil, err
	}

	var commit *vcs.Commit
	_, err = r.client.Do(req, &commit)
	if err != nil {
		return nil, err
	}

	return commit, nil
}

// TotalCommitsHeader is the name of the HTTP header that contains the
// total number of commits in a call to Commits.
const TotalCommitsHeader = "x-vcsstore-total-commits"

func (r *repository) Commits(opt vcs.CommitsOptions) ([]*vcs.Commit, uint, error) {
	url, err := r.url(RouteRepoCommits, nil, opt)
	if err != nil {
		return nil, 0, err
	}

	req, err := r.client.NewRequest("GET", url.String(), nil)
	if err != nil {
		return nil, 0, err
	}

	var commits []*vcs.Commit
	resp, err := r.client.Do(req, &commits)
	if err != nil {
		return nil, 0, err
	}

	total, err := strconv.ParseUint(string(resp.Header.Get(TotalCommitsHeader)), 10, 64)
	if err != nil {
		return nil, 0, err
	}

	return commits, uint(total), nil
}

func (r *repository) Committers(opt vcs.CommittersOptions) ([]*vcs.Committer, error) {
	url, err := r.url(RouteRepoCommitters, nil, opt)
	if err != nil {
		return nil, err
	}

	req, err := r.client.NewRequest("GET", url.String(), nil)
	if err != nil {
		return nil, err
	}

	var committers []*vcs.Committer
	_, err = r.client.Do(req, &committers)
	if err != nil {
		return nil, err
	}

	return committers, nil
}

// FileSystem returns a vfs.FileSystem that accesses the repository tree. The
// returned interface also satisfies vcsclient.FileSystem, which has an
// additional Get method that is useful for fetching all information about an
// entry in the tree.
func (r *repository) FileSystem(at vcs.CommitID) (vfs.FileSystem, error) {
	return &repositoryFS{
		at:   at,
		repo: r,
	}, nil
}

// router used to generate URLs for the vcsstore API.
var router = NewRouter(nil)

// url generates the URL to the named vcsstore API endpoint, using the
// specified route variables and query options.
func (r *repository) url(routeName string, routeVars map[string]string, opt interface{}) (*url.URL, error) {
	route := (*muxpkg.Router)(router).Get(routeName)
	if route == nil {
		return nil, fmt.Errorf("no API route named %q", route)
	}

	routeVarsList := make([]string, 2*len(routeVars))
	i := 0
	for name, val := range routeVars {
		routeVarsList[i*2] = name
		routeVarsList[i*2+1] = val
		i++
	}
	routeVarsList = append(routeVarsList, "RepoPath", r.repoPath)
	url, err := route.URL(routeVarsList...)
	if err != nil {
		return nil, err
	}

	// make the route URL path relative to BaseURL by trimming the leading "/"
	url.Path = strings.TrimPrefix(url.Path, "/")

	if opt != nil {
		err = addOptions(url, opt)
		if err != nil {
			return nil, err
		}
	}

	return url, nil
}

// addOptions adds the parameters in opt as URL query parameters to u. opt
// must be a struct whose fields may contain "url" tags.
func addOptions(u *url.URL, opt interface{}) error {
	v := reflect.ValueOf(opt)
	if v.Kind() == reflect.Ptr && v.IsNil() {
		return nil
	}

	qs, err := query.Values(opt)
	if err != nil {
		return err
	}

	u.RawQuery = qs.Encode()
	return nil
}
<commit_msg>added Repository.Close<commit_after>package vcsclient

import (
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"reflect"
	"strconv"
	"strings"

	"github.com/google/go-querystring/query"
	muxpkg "github.com/sourcegraph/mux"
	"golang.org/x/tools/godoc/vfs"
	"sourcegraph.com/sourcegraph/go-vcs/vcs"
)

var ErrRepoNotExist = errors.New("repository does not exist on remote server")

func IsRepoNotExist(err error) bool {
	if err == nil {
		return false
	}
	if err == ErrRepoNotExist {
		return true
	}
	if err, ok := err.(*ErrorResponse); ok {
		return err.Message == ErrRepoNotExist.Error()
	}
	return err.Error() == ErrRepoNotExist.Error()
}

type repository struct {
	client   *Client
	repoPath string
}

var _ vcs.Repository = (*repository)(nil)

type RepositoryCloneUpdater interface {
	// CloneOrUpdate instructs the server to clone the repository so
	// it is available to the client via the API if it doesn't yet
	// exist, or update it from its default remote. The call blocks
	// until cloning finishes or fails.
	CloneOrUpdate(cloneInfo *CloneInfo) error
}

// CloneInfo is the information needed to clone a repository.
type CloneInfo struct {
	// VCS is the type of VCS (e.g., "git")
	VCS string

	// CloneURL is the remote URL from which to clone.
	CloneURL string

	// Additional options
	vcs.RemoteOpts
}

func (r *repository) Close() error {
	// ignored, because each remote call is stateless
	return nil
}

func (r *repository) CloneOrUpdate(cloneInfo *CloneInfo) error {
	url, err := r.url(RouteRepo, nil, nil)
	if err != nil {
		return err
	}

	req, err := r.client.NewRequest("POST", url.String(), cloneInfo)
	if err != nil {
		return err
	}

	resp, err := r.client.Do(req, nil)
	if err != nil {
		return err
	}
	if c := resp.StatusCode; c != http.StatusOK && c != http.StatusCreated {
		return fmt.Errorf("CloneOrUpdate: HTTP error %d", c)
	}

	return nil
}

func (r *repository) ResolveBranch(name string) (vcs.CommitID, error) {
	url, err := r.url(RouteRepoBranch, map[string]string{"Branch": name}, nil)
	if err != nil {
		return "", err
	}

	req, err := r.client.NewRequest("GET", url.String(), nil)
	if err != nil {
		return "", err
	}

	resp, err := r.client.doIgnoringRedirects(req)
	if err != nil {
		return "", err
	}

	return r.parseCommitIDInURL(resp.Header.Get("location"))
}

func (r *repository) ResolveRevision(spec string) (vcs.CommitID, error) {
	url, err := r.url(RouteRepoRevision, map[string]string{"RevSpec": spec}, nil)
	if err != nil {
		return "", err
	}

	req, err := r.client.NewRequest("GET", url.String(), nil)
	if err != nil {
		return "", err
	}

	resp, err := r.client.doIgnoringRedirects(req)
	if err != nil {
		return "", err
	}

	return r.parseCommitIDInURL(resp.Header.Get("location"))
}

func (r *repository) ResolveTag(name string) (vcs.CommitID, error) {
	url, err := r.url(RouteRepoTag, map[string]string{"Tag": name}, nil)
	if err != nil {
		return "", err
	}

	req, err := r.client.NewRequest("GET", url.String(), nil)
	if err != nil {
		return "", err
	}

	resp, err := r.client.doIgnoringRedirects(req)
	if err != nil {
		return "", err
	}

	return r.parseCommitIDInURL(resp.Header.Get("location"))
}

func (r *repository) parseCommitIDInURL(urlStr string) (vcs.CommitID, error) {
	url, err := url.Parse(urlStr)
	if err != nil {
		return "", err
	}

	var info muxpkg.RouteMatch
	match := (*muxpkg.Router)(router).Match(&http.Request{Method: "GET", URL: url}, &info)
	if !match || info.Vars["CommitID"] == "" {
		return "", errors.New("failed to determine CommitID from URL")
	}

	return vcs.CommitID(info.Vars["CommitID"]), nil
}

func (r *repository) Branches(opt vcs.BranchesOptions) ([]*vcs.Branch, error) {
	url, err := r.url(RouteRepoBranches, nil, opt)
	if err != nil {
		return nil, err
	}

	req, err := r.client.NewRequest("GET", url.String(), nil)
	if err != nil {
		return nil, err
	}

	var branches []*vcs.Branch
	_, err = r.client.Do(req, &branches)
	if err != nil {
		return nil, err
	}

	return branches, nil
}

func (r *repository) Tags() ([]*vcs.Tag, error) {
	url, err := r.url(RouteRepoTags, nil, nil)
	if err != nil {
		return nil, err
	}

	req, err := r.client.NewRequest("GET", url.String(), nil)
	if err != nil {
		return nil, err
	}

	var tags []*vcs.Tag
	_, err = r.client.Do(req, &tags)
	if err != nil {
		return nil, err
	}

	return tags, nil
}

func (r *repository) GetCommit(id vcs.CommitID) (*vcs.Commit, error) {
	url, err := r.url(RouteRepoCommit, map[string]string{"CommitID": string(id)}, nil)
	if err != nil {
		return nil, err
	}

	req, err := r.client.NewRequest("GET", url.String(), nil)
	if err != nil {
		return nil, err
	}

	var commit *vcs.Commit
	_, err = r.client.Do(req, &commit)
	if err != nil {
		return nil, err
	}

	return commit, nil
}

// TotalCommitsHeader is the name of the HTTP header that contains the
// total number of commits in a call to Commits.
const TotalCommitsHeader = "x-vcsstore-total-commits"

func (r *repository) Commits(opt vcs.CommitsOptions) ([]*vcs.Commit, uint, error) {
	url, err := r.url(RouteRepoCommits, nil, opt)
	if err != nil {
		return nil, 0, err
	}

	req, err := r.client.NewRequest("GET", url.String(), nil)
	if err != nil {
		return nil, 0, err
	}

	var commits []*vcs.Commit
	resp, err := r.client.Do(req, &commits)
	if err != nil {
		return nil, 0, err
	}

	total, err := strconv.ParseUint(string(resp.Header.Get(TotalCommitsHeader)), 10, 64)
	if err != nil {
		return nil, 0, err
	}

	return commits, uint(total), nil
}

func (r *repository) Committers(opt vcs.CommittersOptions) ([]*vcs.Committer, error) {
	url, err := r.url(RouteRepoCommitters, nil, opt)
	if err != nil {
		return nil, err
	}

	req, err := r.client.NewRequest("GET", url.String(), nil)
	if err != nil {
		return nil, err
	}

	var committers []*vcs.Committer
	_, err = r.client.Do(req, &committers)
	if err != nil {
		return nil, err
	}

	return committers, nil
}

// FileSystem returns a vfs.FileSystem that accesses the repository tree. The
// returned interface also satisfies vcsclient.FileSystem, which has an
// additional Get method that is useful for fetching all information about an
// entry in the tree.
func (r *repository) FileSystem(at vcs.CommitID) (vfs.FileSystem, error) {
	return &repositoryFS{
		at:   at,
		repo: r,
	}, nil
}

// router used to generate URLs for the vcsstore API.
var router = NewRouter(nil)

// url generates the URL to the named vcsstore API endpoint, using the
// specified route variables and query options.
func (r *repository) url(routeName string, routeVars map[string]string, opt interface{}) (*url.URL, error) {
	route := (*muxpkg.Router)(router).Get(routeName)
	if route == nil {
		return nil, fmt.Errorf("no API route named %q", route)
	}

	routeVarsList := make([]string, 2*len(routeVars))
	i := 0
	for name, val := range routeVars {
		routeVarsList[i*2] = name
		routeVarsList[i*2+1] = val
		i++
	}
	routeVarsList = append(routeVarsList, "RepoPath", r.repoPath)
	url, err := route.URL(routeVarsList...)
	if err != nil {
		return nil, err
	}

	// make the route URL path relative to BaseURL by trimming the leading "/"
	url.Path = strings.TrimPrefix(url.Path, "/")

	if opt != nil {
		err = addOptions(url, opt)
		if err != nil {
			return nil, err
		}
	}

	return url, nil
}

// addOptions adds the parameters in opt as URL query parameters to u. opt
// must be a struct whose fields may contain "url" tags.
func addOptions(u *url.URL, opt interface{}) error {
	v := reflect.ValueOf(opt)
	if v.Kind() == reflect.Ptr && v.IsNil() {
		return nil
	}

	qs, err := query.Values(opt)
	if err != nil {
		return err
	}

	u.RawQuery = qs.Encode()
	return nil
}
<|endoftext|>


<commit_before>// Copyright 2016 The nvim-go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package delve

import (
	"net"
	"os/exec"

	"nvim-go/nvimutil"

	"github.com/neovim/go-client/nvim"
	"github.com/pkg/errors"
)

type Config struct {
	addr  string
	flags []string
	path  string
	pid   int
}

// startServer starts the delve headless server and replace server Stdout & Stderr.
func (d *Delve) startServer(cmd string, cfg Config) error {
	dlvBin, err := exec.LookPath("dlv")
	if err != nil {
		return errors.WithStack(err)
	}

	switch cmd {
	case "attach":
		// TODO(zchee): implements
	case "connect":
		// connect command must be addr to the second argument
		d.server = exec.Command(dlvBin, cmd, cfg.addr, "--log")
	case "debug":
		// debug command must be package path to the second argument, and need "--accept-multiclient" flag
		d.server = exec.Command(dlvBin, cmd, cfg.path, "--headless", "--listen="+cfg.addr, "--accept-multiclient", "--api-version=2", "--log")
	case "exec":
		// TODO(zchee): implements
	case "test":
		// TODO(zchee): implements
	case "trace":
		// TODO(zchee): implements
	}
	// append other flags such as build flags
	d.server.Args = append(d.server.Args, cfg.flags...)

	if err := d.server.Start(); err != nil {
		err = errors.New(d.serverOut.String())
		d.serverOut.Reset()
		return errors.WithStack(err)
	}

	return nil
}

// dialServer dial the dlv launch the headless server.
// `net.Dial` is better way?
// http://stackoverflow.com/a/30838807/5228839
func (d *Delve) dialServer(v *nvim.Nvim, addr string) error {
	defer nvimutil.EchohlAfter(v, "Delve", nvimutil.ProgressColor, "Ready")
	nvimutil.EchoProgress(v, "Delve", "Wait for running dlv server")

	for {
		conn, err := net.Dial("tcp", addr)
		if err != nil {
			continue
		}
		defer conn.Close()
		break
	}

	return nil
}
<commit_msg>commands/delve: rename dlvBin variable to dlv<commit_after>// Copyright 2016 The nvim-go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package delve

import (
	"net"
	"os/exec"

	"nvim-go/nvimutil"

	"github.com/neovim/go-client/nvim"
	"github.com/pkg/errors"
)

type Config struct {
	addr  string
	flags []string
	path  string
	pid   int
}

// startServer starts the delve headless server and replace server Stdout & Stderr.
func (d *Delve) startServer(cmd string, cfg Config) error {
	dlv, err := exec.LookPath("dlv")
	if err != nil {
		return errors.WithStack(err)
	}

	switch cmd {
	case "attach":
		// TODO(zchee): implements
	case "connect":
		// connect command must be addr to the second argument
		d.server = exec.Command(dlv, cmd, cfg.addr, "--log")
	case "debug":
		// debug command must be package path to the second argument, and need "--accept-multiclient" flag
		d.server = exec.Command(dlv, cmd, cfg.path, "--headless", "--listen="+cfg.addr, "--accept-multiclient", "--api-version=2", "--log")
	case "exec":
		// TODO(zchee): implements
	case "test":
		// TODO(zchee): implements
	case "trace":
		// TODO(zchee): implements
	}
	// append other flags such as build flags
	d.server.Args = append(d.server.Args, cfg.flags...)

	if err := d.server.Start(); err != nil {
		err = errors.New(d.serverOut.String())
		d.serverOut.Reset()
		return errors.WithStack(err)
	}

	return nil
}

// dialServer dial the dlv launch the headless server.
// `net.Dial` is better way?
// http://stackoverflow.com/a/30838807/5228839
func (d *Delve) dialServer(v *nvim.Nvim, addr string) error {
	defer nvimutil.EchohlAfter(v, "Delve", nvimutil.ProgressColor, "Ready")
	nvimutil.EchoProgress(v, "Delve", "Wait for running dlv server")

	for {
		conn, err := net.Dial("tcp", addr)
		if err != nil {
			continue
		}
		defer conn.Close()
		break
	}

	return nil
}
<|endoftext|>


<commit_before>// Copyright 2016 The nvim-go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package nvimutil

import (
	"bytes"

	"fmt"
	"go/token"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"

	"nvim-go/config"
	"nvim-go/context"
	"nvim-go/pathutil"

	"github.com/neovim/go-client/nvim"
	"github.com/pkg/errors"
)

// SetLoclist set the error results data to current buffer's locationlist.
func SetLoclist(v *nvim.Nvim, loclist []*nvim.QuickfixError) error {
	// setloclist({nr}, {list} [, {action}])
	// v.Call(fname string, result interface{}, args ...interface{})
	if len(loclist) > 0 {
		v.Call("setloclist", nil, 0, loclist)
	} else {
		v.Command("lexpr ''")
	}

	return nil
}

var (
	listtype     ErrorListType
	openlistCmd  func() error
	closelistCmd func() error
	clearlistCmd func() error
	setlistCmd   func(errlist []*nvim.QuickfixError) error
)

type ErrorListType string

const (
	Quickfix     ErrorListType = "quickfix"
	LocationList               = "locationlist"
)

func getListCmd(v *nvim.Nvim) {
	if listtype == "" {
		listtype = ErrorListType(config.ErrorListType)
	}
	switch listtype {
	case Quickfix:
		openlistCmd = func() error { return v.Command("copen") }
		closelistCmd = func() error { return v.Command("cclose") }
		clearlistCmd = func() error { return v.Command("cgetexpr ''") }
		setlistCmd = func(errlist []*nvim.QuickfixError) error { return v.Call("setqflist", nil, errlist, "r") }
	case LocationList:
		openlistCmd = func() error { return v.Command("lopen") }
		closelistCmd = func() error { return v.Command("lclose") }
		clearlistCmd = func() error { return v.Command("lgetexpr ''") }
		setlistCmd = func(errlist []*nvim.QuickfixError) error { return v.Call("setloclist", nil, 0, errlist, "r") }
	}
}

// ErrorList merges the errlist map items and open the locationlist window.
// TODO(zchee): This function will reports the errors with open the quickfix window, but will close
// the quickfix window if no errors.
// Do ErrorList function name is appropriate?
func ErrorList(v *nvim.Nvim, errors map[string][]*nvim.QuickfixError, keep bool) error {
	if listtype == "" {
		getListCmd(v)
	}

	if errors == nil || len(errors) == 0 {
		defer clearlistCmd()
		return closelistCmd()
	}

	var errlist []*nvim.QuickfixError
	for _, err := range errors {
		errlist = append(errlist, err...)
	}
	if err := SetErrorlist(v, errlist); err != nil {
		return err
	}

	if keep {
		w, err := v.CurrentWindow()
		if err != nil {
			return err
		}
		defer v.SetCurrentWindow(w)
	}
	return openlistCmd()
}

// SetErrorlist set the error results data to Neovim error list.
func SetErrorlist(v *nvim.Nvim, errlist []*nvim.QuickfixError) error {
	if setlistCmd == nil {
		getListCmd(v)
	}

	return setlistCmd(errlist)
}

// ClearErrorlist clear the Neovim error list.
func ClearErrorlist(v *nvim.Nvim, close bool) error {
	if clearlistCmd == nil {
		getListCmd(v)
	}

	if close {
		defer closelistCmd()
	}
	return clearlistCmd()
}

// OpenLoclist open or close the current buffer's locationlist window.
func OpenLoclist(v *nvim.Nvim, w nvim.Window, loclist []*nvim.QuickfixError, keep bool) error {
	if len(loclist) == 0 {
		return v.Command("lclose")
	}

	v.Command("lopen")
	if keep {
		return v.SetCurrentWindow(w)
	}
	return nil
}

// CloseLoclist close the current buffer's locationlist window.
func CloseLoclist(v *nvim.Nvim) error {
	return v.Command("lclose")
}

// SetQuickfix set the error results data to quickfix list.
func SetQuickfix(p *nvim.Pipeline, qflist []*nvim.QuickfixError) error {
	p.Call("setqflist", nil, qflist)

	return nil
}

// OpenOuickfix open the quickfix list window.
func OpenOuickfix(p *nvim.Pipeline, w nvim.Window, keep bool) error {
	p.Command("copen")
	if keep {
		p.SetCurrentWindow(w)
	}
	if err := p.Wait(); err != nil {
		return err
	}

	return nil
}

// CloseQuickfix close the quickfix list window.
func CloseQuickfix(v *nvim.Nvim) error {
	return v.Command("cclose")
}

func GotoPos(v *nvim.Nvim, w nvim.Window, pos token.Position, cwd string) error {
	fname, line, col := SplitPos(pos.String(), cwd)

	v.Command(fmt.Sprintf("edit %s", fname))
	v.SetWindowCursor(w, [2]int{line, col - 1})
	defer v.Command(`lclose`)

	return v.Command(`normal zz`)
}

// SplitPos parses a string of form 'token.Pos', and return the relative
// filepath from the current working directory path.
func SplitPos(pos string, cwd string) (string, int, int) {
	position := strings.Split(pos, ":")

	fname := position[0]
	line, err := strconv.ParseInt(position[1], 10, 64)
	if err != nil {
		line = 0
	}
	col, err := strconv.ParseInt(position[2], 10, 64)
	if err != nil {
		col = 0
	}

	if strings.HasPrefix(fname, cwd) {
		frel := strings.TrimPrefix(fname, cwd+string(filepath.Separator))
		if fname != frel {
			return frel, int(line), int(col)
		}
	}

	return fname, int(line), int(col)
}

// regexp pattern: https://regex101.com/r/bUVZpH/2
var errRe = regexp.MustCompile(`(?m)^(?:#\s([[:graph:]]+))?(?:[\s\t]+)?([^\s:]+):(\d+)(?::(\d+))?(?::)?\s(.*)`)

// ParseError parses a typical Go tools error messages.
func ParseError(errs []byte, cwd string, ctxt *context.Build) ([]*nvim.QuickfixError, error) {
	var (
		// packagePath for the save the error files parent directory.
		// It will be re-assigned if "# " is in the error message.
		packagePath string
		errlist     []*nvim.QuickfixError
	)

	// m[1]: package path with "# " prefix
	// m[2]: error files relative path
	// m[3]: line number of error point
	// m[4]: column number of error point
	// m[5]: error description text
	for _, m := range errRe.FindAllSubmatch(errs, -1) {
		if m[1] != nil {
			// Save the package path for the second subsequent errors
			packagePath = string(m[1])
		}
		filename := string(m[2])

		// Avoid the local package error. like "package foo" and edit "cmd/foo/main.go"
		if !strings.Contains(filename, "../") {
			if !filepath.IsAbs(filename) && packagePath != "" {
				// Joins the packagePath and error file
				filename = filepath.Join(packagePath, filepath.Base(filename))
			}

			// Cleanup filename to relative path of current working directory
			switch ctxt.Tool {
			case "go":
				var sep string
				switch {
				// filename has not directory path
				case filepath.Dir(filename) == ".":
					filename = filepath.Join(cwd, filename)

				// not contains '#' package title in errror
				case strings.HasPrefix(filename, cwd):
					sep = cwd
					filename = strings.TrimPrefix(filename, sep+string(filepath.Separator))

				// filename is like "github.com/foo/bar.go"
				case strings.HasPrefix(filename, pathutil.TrimGoPath(cwd)):
					sep = pathutil.TrimGoPath(cwd) + string(filepath.Separator)
					filename = strings.TrimPrefix(filename, sep)
				}
			case "gb":
				// gb compiler error messages is relative filename path of project root dir
				if !filepath.IsAbs(filename) {
					filename = filepath.Join(ctxt.ProjectRoot, "src", filename)
				}
			default:
				return nil, errors.New("unknown compiler tool")
			}
		}

		// Finally, try to convert the relative path from cwd
		filename = pathutil.Rel(cwd, filename)

		// line is necessary for error messages
		line, err := strconv.Atoi(string(m[3]))
		if err != nil {
			return nil, err
		}

		// Ignore err because fail strconv.Atoi will assign 0 to col
		col, _ := strconv.Atoi(string(m[4]))

		errlist = append(errlist, &nvim.QuickfixError{
			FileName: filename,
			LNum:     line,
			Col:      col,
			Text:     string(bytes.TrimSpace(m[5])),
		})
	}

	return errlist, nil
}
<commit_msg>nvimutil/locationlist: remove contains the '../' check on ParseError<commit_after>// Copyright 2016 The nvim-go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package nvimutil

import (
	"bytes"

	"fmt"
	"go/token"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"

	"nvim-go/config"
	"nvim-go/context"
	"nvim-go/pathutil"

	"github.com/neovim/go-client/nvim"
	"github.com/pkg/errors"
)

// SetLoclist set the error results data to current buffer's locationlist.
func SetLoclist(v *nvim.Nvim, loclist []*nvim.QuickfixError) error {
	// setloclist({nr}, {list} [, {action}])
	// v.Call(fname string, result interface{}, args ...interface{})
	if len(loclist) > 0 {
		v.Call("setloclist", nil, 0, loclist)
	} else {
		v.Command("lexpr ''")
	}

	return nil
}

var (
	listtype     ErrorListType
	openlistCmd  func() error
	closelistCmd func() error
	clearlistCmd func() error
	setlistCmd   func(errlist []*nvim.QuickfixError) error
)

type ErrorListType string

const (
	Quickfix     ErrorListType = "quickfix"
	LocationList               = "locationlist"
)

func getListCmd(v *nvim.Nvim) {
	if listtype == "" {
		listtype = ErrorListType(config.ErrorListType)
	}
	switch listtype {
	case Quickfix:
		openlistCmd = func() error { return v.Command("copen") }
		closelistCmd = func() error { return v.Command("cclose") }
		clearlistCmd = func() error { return v.Command("cgetexpr ''") }
		setlistCmd = func(errlist []*nvim.QuickfixError) error { return v.Call("setqflist", nil, errlist, "r") }
	case LocationList:
		openlistCmd = func() error { return v.Command("lopen") }
		closelistCmd = func() error { return v.Command("lclose") }
		clearlistCmd = func() error { return v.Command("lgetexpr ''") }
		setlistCmd = func(errlist []*nvim.QuickfixError) error { return v.Call("setloclist", nil, 0, errlist, "r") }
	}
}

// ErrorList merges the errlist map items and open the locationlist window.
// TODO(zchee): This function will reports the errors with open the quickfix window, but will close
// the quickfix window if no errors.
// Do ErrorList function name is appropriate?
func ErrorList(v *nvim.Nvim, errors map[string][]*nvim.QuickfixError, keep bool) error {
	if listtype == "" {
		getListCmd(v)
	}

	if errors == nil || len(errors) == 0 {
		defer clearlistCmd()
		return closelistCmd()
	}

	var errlist []*nvim.QuickfixError
	for _, err := range errors {
		errlist = append(errlist, err...)
	}
	if err := SetErrorlist(v, errlist); err != nil {
		return err
	}

	if keep {
		w, err := v.CurrentWindow()
		if err != nil {
			return err
		}
		defer v.SetCurrentWindow(w)
	}
	return openlistCmd()
}

// SetErrorlist set the error results data to Neovim error list.
func SetErrorlist(v *nvim.Nvim, errlist []*nvim.QuickfixError) error {
	if setlistCmd == nil {
		getListCmd(v)
	}

	return setlistCmd(errlist)
}

// ClearErrorlist clear the Neovim error list.
func ClearErrorlist(v *nvim.Nvim, close bool) error {
	if clearlistCmd == nil {
		getListCmd(v)
	}

	if close {
		defer closelistCmd()
	}
	return clearlistCmd()
}

// OpenLoclist open or close the current buffer's locationlist window.
func OpenLoclist(v *nvim.Nvim, w nvim.Window, loclist []*nvim.QuickfixError, keep bool) error {
	if len(loclist) == 0 {
		return v.Command("lclose")
	}

	v.Command("lopen")
	if keep {
		return v.SetCurrentWindow(w)
	}
	return nil
}

// CloseLoclist close the current buffer's locationlist window.
func CloseLoclist(v *nvim.Nvim) error {
	return v.Command("lclose")
}

// SetQuickfix set the error results data to quickfix list.
func SetQuickfix(p *nvim.Pipeline, qflist []*nvim.QuickfixError) error {
	p.Call("setqflist", nil, qflist)

	return nil
}

// OpenOuickfix open the quickfix list window.
func OpenOuickfix(p *nvim.Pipeline, w nvim.Window, keep bool) error {
	p.Command("copen")
	if keep {
		p.SetCurrentWindow(w)
	}
	if err := p.Wait(); err != nil {
		return err
	}

	return nil
}

// CloseQuickfix close the quickfix list window.
func CloseQuickfix(v *nvim.Nvim) error {
	return v.Command("cclose")
}

func GotoPos(v *nvim.Nvim, w nvim.Window, pos token.Position, cwd string) error {
	fname, line, col := SplitPos(pos.String(), cwd)

	v.Command(fmt.Sprintf("edit %s", fname))
	v.SetWindowCursor(w, [2]int{line, col - 1})
	defer v.Command(`lclose`)

	return v.Command(`normal zz`)
}

// SplitPos parses a string of form 'token.Pos', and return the relative
// filepath from the current working directory path.
func SplitPos(pos string, cwd string) (string, int, int) {
	position := strings.Split(pos, ":")

	fname := position[0]
	line, err := strconv.ParseInt(position[1], 10, 64)
	if err != nil {
		line = 0
	}
	col, err := strconv.ParseInt(position[2], 10, 64)
	if err != nil {
		col = 0
	}

	if strings.HasPrefix(fname, cwd) {
		frel := strings.TrimPrefix(fname, cwd+string(filepath.Separator))
		if fname != frel {
			return frel, int(line), int(col)
		}
	}

	return fname, int(line), int(col)
}

// regexp pattern: https://regex101.com/r/bUVZpH/2
var errRe = regexp.MustCompile(`(?m)^(?:#\s([[:graph:]]+))?(?:[\s\t]+)?([^\s:]+):(\d+)(?::(\d+))?(?::)?\s(.*)`)

// ParseError parses a typical Go tools error messages.
func ParseError(errs []byte, cwd string, ctxt *context.Build) ([]*nvim.QuickfixError, error) {
	var (
		// packagePath for the save the error files parent directory.
		// It will be re-assigned if "# " is in the error message.
		packagePath string
		errlist     []*nvim.QuickfixError
	)

	// m[1]: package path with "# " prefix
	// m[2]: error files relative path
	// m[3]: line number of error point
	// m[4]: column number of error point
	// m[5]: error description text
	for _, m := range errRe.FindAllSubmatch(errs, -1) {
		if m[1] != nil {
			// Save the package path for the second subsequent errors
			packagePath = string(m[1])
		}
		filename := string(m[2])

		// Avoid the local package error. like "package foo" and edit "cmd/foo/main.go"
		if !filepath.IsAbs(filename) && packagePath != "" {
			// Joins the packagePath and error file
			filename = filepath.Join(packagePath, filepath.Base(filename))
		}

		// Cleanup filename to relative path of current working directory
		switch ctxt.Tool {
		case "go":
			var sep string
			switch {
			case pathutil.IsExist(pathutil.JoinGoPath(filename)):
				filename = pathutil.JoinGoPath(filename)

			// filename has not directory path
			case filepath.Dir(filename) == ".":
				filename = filepath.Join(cwd, filename)

			// not contains '#' package title in errror
			case strings.HasPrefix(filename, cwd):
				sep = cwd
				filename = strings.TrimPrefix(filename, sep+string(filepath.Separator))

			// filename is like "github.com/foo/bar.go"
			case strings.HasPrefix(filename, pathutil.TrimGoPath(cwd)):
				sep = pathutil.TrimGoPath(cwd) + string(filepath.Separator)
				filename = strings.TrimPrefix(filename, sep)
			}
		case "gb":
			// gb compiler error messages is relative filename path of project root dir
			if !filepath.IsAbs(filename) {
				filename = filepath.Join(ctxt.ProjectRoot, "src", filename)
			}
		default:
			return nil, errors.New("unknown compiler tool")
		}

		// Finally, try to convert the relative path from cwd
		filename = pathutil.Rel(cwd, filename)

		// line is necessary for error messages
		line, err := strconv.Atoi(string(m[3]))
		if err != nil {
			return nil, err
		}

		// Ignore err because fail strconv.Atoi will assign 0 to col
		col, _ := strconv.Atoi(string(m[4]))

		errlist = append(errlist, &nvim.QuickfixError{
			FileName: filename,
			LNum:     line,
			Col:      col,
			Text:     string(bytes.TrimSpace(m[5])),
		})
	}

	return errlist, nil
}
<|endoftext|>