shithub: jirafs

Download patch

ref: ef379bb73ac216792ac1601ec6589cbe09ccfeba
parent: 0c729fb8280bf2bc2dd12952873bd82f20247c69
author: Kenny Levinsen <w@kl.wtf>
date: Wed Jun 8 18:31:33 EDT 2016

Refactor

--- /dev/null
+++ b/client.go
@@ -1,0 +1,207 @@
+package main
+
+import (
+	"bytes"
+	"crypto/tls"
+	"crypto/x509"
+	"encoding/json"
+	"encoding/pem"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+
+	"github.com/mrjones/oauth"
+)
+
+type Client struct {
+	*http.Client
+
+	user, pass              string
+	jiraURL                 *url.URL
+	cookies                 []*http.Cookie
+	alwaysLogin, usingOAuth bool
+
+	maxIssueListing int
+}
+
+type RPCError struct {
+	Status      string
+	Body        []byte
+	Description string
+}
+
+func (rpc *RPCError) Error() string {
+	return fmt.Sprintf("RPCError: %s: status %d,  %s", rpc.Description, rpc.Status, rpc.Body)
+}
+
+func (c *Client) RPC(method, path string, body, target interface{}) error {
+	u, err := c.jiraURL.Parse(path)
+	if err != nil {
+		return err
+	}
+
+	var b io.Reader
+	if body != nil {
+		buf, err := json.Marshal(body)
+		if err != nil {
+			return err
+		}
+		b = bytes.NewReader(buf)
+
+	}
+
+	req, err := http.NewRequest(method, u.String(), b)
+	if err != nil {
+		return err
+	}
+
+	if body != nil {
+		req.Header.Set("Content-Type", "application/json")
+	}
+	req.Header.Set("X-Atlassian-Token", "nocheck")
+
+	if c.alwaysLogin && !c.usingOAuth {
+		if err := c.AcquireSessionCookie(c.user, c.pass); err != nil {
+			return err
+		}
+	}
+
+	for _, cookie := range c.cookies {
+		req.AddCookie(cookie)
+	}
+
+	resp, err := c.Client.Do(req)
+	if err != nil {
+		return err
+	}
+
+	respBody, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return err
+	}
+	resp.Body.Close()
+
+	if !(resp.StatusCode >= 200 && resp.StatusCode <= 299) {
+		err = &RPCError{
+			Description: "request failed",
+			Status:      resp.Status,
+			Body:        respBody,
+		}
+		return err
+	}
+
+	if target != nil {
+		if err := json.Unmarshal(respBody, target); err != nil {
+			return err
+		}
+	}
+
+	return nil
+
+}
+
+func (c *Client) AcquireSessionCookie(username, password string) error {
+	url, err := c.jiraURL.Parse("/rest/auth/1/session")
+	if err != nil {
+		return err
+	}
+
+	body := struct {
+		Username string `json:"username"`
+		Password string `json:"password"`
+	}{username, password}
+	b, err := json.Marshal(body)
+	if err != nil {
+		return err
+	}
+
+	req, err := http.NewRequest("POST", url.String(), bytes.NewReader(b))
+	if err != nil {
+		return err
+	}
+	req.Header.Set("Content-Type", "application/json")
+
+	resp, err := c.Client.Do(req)
+	if _, err := ioutil.ReadAll(resp.Body); err != nil {
+		return err
+	}
+	resp.Body.Close()
+	c.cookies = resp.Cookies()
+
+	if err != nil {
+		return fmt.Errorf("Auth at JIRA instance failed (HTTP(S) request). %s", err)
+	}
+	if resp != nil && resp.StatusCode != 200 {
+		return fmt.Errorf("Auth at JIRA instance failed (HTTP(S) request). Status code: %d", resp.StatusCode)
+	}
+
+	return nil
+}
+
+func (c *Client) login() error {
+	if c.usingOAuth {
+		return nil
+	}
+	if err := c.AcquireSessionCookie(c.user, c.pass); err != nil {
+		return fmt.Errorf("Could not authenticate to JIRA: %v\n", err)
+	}
+	return nil
+}
+
+func (c *Client) oauth(consumerKey, privateKeyFile string) error {
+	pvf, err := ioutil.ReadFile(privateKeyFile)
+	if err != nil {
+		return err
+	}
+
+	block, _ := pem.Decode(pvf)
+	privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
+	if err != nil {
+		return err
+	}
+
+	url1, _ := c.jiraURL.Parse("/plugins/servlet/oauth/request-token")
+	url2, _ := c.jiraURL.Parse("/plugins/servlet/oauth/authorize")
+	url3, _ := c.jiraURL.Parse("/plugins/servlet/oauth/access-token")
+
+	t := oauth.NewRSAConsumer(
+		consumerKey,
+		privateKey,
+		oauth.ServiceProvider{
+			RequestTokenUrl:   url1.String(),
+			AuthorizeTokenUrl: url2.String(),
+			AccessTokenUrl:    url3.String(),
+			HttpMethod:        "POST",
+		},
+	)
+
+	t.HttpClient = &http.Client{
+		Transport: &http.Transport{
+			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+		},
+	}
+
+	requestToken, url, err := t.GetRequestTokenAndUrl("oob")
+	if err != nil {
+		return err
+	}
+
+	fmt.Printf("OAuth token requested. Please to go the following URL:\n\t%s\n\nEnter verification code: ", url)
+	var verificationCode string
+	fmt.Scanln(&verificationCode)
+	accessToken, err := t.AuthorizeToken(requestToken, verificationCode)
+	if err != nil {
+		return err
+	}
+	fmt.Printf("OAuth token authorized.\n")
+
+	client, err := t.MakeHttpClient(accessToken)
+	if err != nil {
+		return err
+	}
+
+	c.Client = client
+	return nil
+}
--- /dev/null
+++ b/files.go
@@ -1,0 +1,189 @@
+package main
+
+import (
+	"errors"
+	"fmt"
+	"log"
+	"strings"
+	"time"
+
+	"github.com/joushou/qp"
+	"github.com/joushou/qptools/fileserver/trees"
+)
+
+type jiraWalker interface {
+	Walk(jc *Client, name string) (trees.File, error)
+}
+
+type jiraLister interface {
+	List(jc *Client) ([]qp.Stat, error)
+}
+
+type jiraRemover interface {
+	Remove(jc *Client, name string) error
+}
+
+// JiraDir is a convenience wrapper for dynamic directory hooks.
+type JiraDir struct {
+	thing  interface{}
+	client *Client
+	*trees.SyntheticDir
+}
+
+func (jd *JiraDir) Walk(user, name string) (trees.File, error) {
+
+	if f, ok := jd.thing.(jiraWalker); ok {
+		return f.Walk(jd.client, name)
+	}
+	if f, ok := jd.thing.(trees.Dir); ok {
+		return f.Walk(user, name)
+	}
+
+	return nil, trees.ErrPermissionDenied
+}
+
+func (jd *JiraDir) List(user string) ([]qp.Stat, error) {
+	if f, ok := jd.thing.(jiraLister); ok {
+		return f.List(jd.client)
+	}
+	if f, ok := jd.thing.(trees.Lister); ok {
+		return f.List(user)
+	}
+
+	return nil, trees.ErrPermissionDenied
+}
+
+func (jd *JiraDir) Remove(user, name string) error {
+	if f, ok := jd.thing.(jiraRemover); ok {
+		return f.Remove(jd.client, name)
+	}
+	if f, ok := jd.thing.(trees.Dir); ok {
+		return f.Remove(user, name)
+	}
+
+	return trees.ErrPermissionDenied
+}
+
+func (jd *JiraDir) Create(user, name string, perms qp.FileMode) (trees.File, error) {
+	return nil, trees.ErrPermissionDenied
+}
+
+func (jd *JiraDir) Open(user string, mode qp.OpenMode) (trees.ReadWriteAtCloser, error) {
+	if !jd.CanOpen(user, mode) {
+		return nil, errors.New("access denied")
+	}
+
+	jd.Lock()
+	defer jd.Unlock()
+	jd.Atime = time.Now()
+	jd.Opens++
+	return &trees.ListHandle{
+		Dir:  jd,
+		User: user,
+	}, nil
+}
+
+func NewJiraDir(name string, perm qp.FileMode, user, group string, jc *Client, thing interface{}) (*JiraDir, error) {
+	switch thing.(type) {
+	case trees.Dir, jiraWalker, jiraLister, jiraRemover:
+	default:
+		return nil, fmt.Errorf("unsupported type: %T", thing)
+	}
+
+	return &JiraDir{
+		thing:        thing,
+		client:       jc,
+		SyntheticDir: trees.NewSyntheticDir(name, perm, user, group),
+	}, nil
+}
+
+type CloseSaverHandle struct {
+	onClose func() error
+	trees.ReadWriteAtCloser
+}
+
+func (csh *CloseSaverHandle) Close() error {
+	err := csh.ReadWriteAtCloser.Close()
+	if err != nil {
+		return err
+	}
+
+	if csh.onClose != nil {
+		return csh.onClose()
+	}
+
+	return nil
+}
+
+// CloseSaver calls a callback on save if the file was opened for writing.
+type CloseSaver struct {
+	onClose func() error
+	trees.File
+}
+
+func (cs *CloseSaver) Open(user string, mode qp.OpenMode) (trees.ReadWriteAtCloser, error) {
+	hndl, err := cs.File.Open(user, mode)
+	if err != nil {
+		return nil, err
+	}
+
+	var closer func() error
+
+	switch mode & 3 {
+	case qp.OWRITE, qp.ORDWR:
+		closer = cs.onClose
+	}
+
+	return &CloseSaverHandle{
+		ReadWriteAtCloser: hndl,
+		onClose:           closer,
+	}, nil
+}
+
+func NewCloseSaver(file trees.File, onClose func() error) trees.File {
+	return &CloseSaver{
+		onClose: onClose,
+		File:    file,
+	}
+}
+
+// CommandFile calls commands on write.
+type CommandFile struct {
+	cmds map[string]func([]string) error
+	*trees.SyntheticFile
+}
+
+func (cf *CommandFile) Close() error { return nil }
+func (cf *CommandFile) ReadAt(p []byte, offset int64) (int, error) {
+	return 0, errors.New("cannot read from command file")
+}
+
+func (cf *CommandFile) WriteAt(p []byte, offset int64) (int, error) {
+	args := strings.Split(strings.Trim(string(p), " \n"), " ")
+	cmd := args[0]
+	args = args[1:]
+
+	if f, exists := cf.cmds[cmd]; exists {
+		err := f(args)
+		if err != nil {
+			log.Printf("Command %s failed: %v", cmd, err)
+		}
+		return len(p), err
+	}
+	return len(p), errors.New("no such command")
+}
+
+func (cf *CommandFile) Open(user string, mode qp.OpenMode) (trees.ReadWriteAtCloser, error) {
+	if !cf.CanOpen(user, mode) {
+		return nil, trees.ErrPermissionDenied
+	}
+
+	return cf, nil
+}
+
+func NewCommandFile(name string, perms qp.FileMode, user, group string, cmds map[string]func([]string) error) *CommandFile {
+	return &CommandFile{
+		cmds:          cmds,
+		SyntheticFile: trees.NewSyntheticFile(name, perms, user, group),
+	}
+}
--- a/jira.go
+++ b/jira.go
@@ -15,18 +15,6 @@
 	"github.com/joushou/qptools/fileserver/trees"
 )
 
-type jiraWalker interface {
-	Walk(jc *Client, name string) (trees.File, error)
-}
-
-type jiraLister interface {
-	List(jc *Client) ([]qp.Stat, error)
-}
-
-type jiraRemover interface {
-	Remove(jc *Client, name string) error
-}
-
 type CommentView struct {
 	project string
 	issueNo string
@@ -98,7 +86,8 @@
 }
 
 func (iw *IssueView) normalFiles() (files, dirs []string) {
-	files = []string{"assignee", "creator", "ctl", "description", "type", "key", "reporter", "status", "summary", "labels", "transitions", "priority", "resolution", "raw", "progress", "links", "components"}
+	files = []string{"assignee", "creator", "ctl", "description", "type", "key", "reporter", "status",
+		"summary", "labels", "transition", "priority", "resolution", "raw", "progress", "links", "components"}
 	dirs = []string{"comments"}
 	return
 }
@@ -277,7 +266,7 @@
 			}
 			sf.SetContent([]byte(s))
 		}
-	case "transitions":
+	case "transition":
 		trs, err := GetTransitionsForIssue(jc, issue.Key)
 		if err != nil {
 			log.Printf("Could not get transitions for issue %s: %v", issue.Key, err)
@@ -371,7 +360,7 @@
 			}
 
 			return nil
-		case "transitions":
+		case "transition":
 			sf.Lock()
 			str := string(sf.Content)
 			sf.Unlock()
@@ -478,7 +467,7 @@
 }
 
 func (sw *SearchView) search(jc *Client) error {
-	keys, err := GetKeysForSearch(jc, sw.query, 250)
+	keys, err := GetKeysForSearch(jc, sw.query, jc.maxIssueListing)
 	if err != nil {
 		return err
 	}
@@ -562,7 +551,7 @@
 }
 
 func (pw *ProjectView) List(jc *Client) ([]qp.Stat, error) {
-	keys, err := GetKeysForNIssues(jc, pw.project, 250)
+	keys, err := GetKeysForNIssues(jc, pw.project, jc.maxIssueListing)
 	if err != nil {
 		log.Printf("Could not generate issue list: %v", err)
 		return nil, err
@@ -639,8 +628,28 @@
 				jw.searchLock.Unlock()
 				return nil
 			},
-			"relogin": func(args []string) error {
+			"pass-login": func(args []string) error {
+				if len(args) == 2 {
+					jc.user = args[0]
+					jc.pass = args[1]
+				}
 				return jc.login()
+			},
+			"set": func(args []string) error {
+				if len(args) != 2 {
+					return errors.New("invalid arguments")
+				}
+				switch args[0] {
+				case "max-issues":
+					mi, err := strconv.ParseInt(args[1], 10, 64)
+					if err != nil {
+						return err
+					}
+					jc.maxIssueListing = int(mi)
+					return nil
+				default:
+					return errors.New("unknown variable")
+				}
 			},
 		}
 		return NewCommandFile("ctl", 0777, "jira", "jira", cmds), nil
--- a/main.go
+++ b/main.go
@@ -14,11 +14,14 @@
 )
 
 var (
-	usingOAuth = flag.Bool("oauth", false, "use OAuth 1.0 for authorization")
-	ckey       = flag.String("ckey", "", "consumer key for OAuth")
-	pkey       = flag.String("pkey", "", "private key file for OAuth")
-	pass       = flag.Bool("pass", false, "use password for authorization")
-	jiraURLStr = flag.String("url", "", "jira URL")
+	usingOAuth  = flag.Bool("oauth", false, "use OAuth 1.0 for authorization")
+	ckey        = flag.String("ckey", "", "consumer key for OAuth")
+	pkey        = flag.String("pkey", "", "private key file for OAuth")
+	pass        = flag.Bool("pass", false, "use password for authorization")
+	jiraURLStr  = flag.String("url", "", "jira URL")
+	loginInt    = flag.Int("loginint", 5, "login interval in minutes - 0 disables automatic relogin (password auth only)")
+	alwaysLogin = flag.Bool("alwayslogin", false, "log in on all requests (password auth only)")
+	maxIssues   = flag.Int("maxissues", 100, "max issue listing")
 )
 
 func main() {
@@ -30,31 +33,39 @@
 		return
 	}
 
-	client := &Client{Client: &http.Client{}, usingOAuth: *usingOAuth, jiraURL: jiraURL}
+	client := &Client{
+		Client:          &http.Client{},
+		alwaysLogin:     *alwaysLogin,
+		usingOAuth:      *usingOAuth,
+		jiraURL:         jiraURL,
+		maxIssueListing: *maxIssues,
+	}
 
 	switch {
 	case *pass:
-		var user string
+		var username string
 		fmt.Printf("Username: ")
-		_, err = fmt.Scanln(&user)
+		_, err = fmt.Scanln(&username)
 		if err == nil {
 			fmt.Printf("Password: ")
-			pass, err := gopass.GetPasswdMasked()
+			password, err := gopass.GetPasswdMasked()
 			if err != nil {
-				fmt.Printf("Could not read password: %v", err)
+				fmt.Printf("Could not read password: %v\n", err)
 				return
 			}
 
-			client.user = user
-			client.pass = string(pass)
+			client.user = username
+			client.pass = string(password)
 			client.login()
 
-			go func() {
-				t := time.NewTicker(5 * time.Minute)
-				for range t.C {
-					client.login()
-				}
-			}()
+			if *loginInt > 0 {
+				go func() {
+					t := time.NewTicker(time.Duration(*loginInt) * time.Minute)
+					for range t.C {
+						client.login()
+					}
+				}()
+			}
 		} else {
 			fmt.Printf("Continuing without authentication.\n")
 		}
@@ -63,11 +74,13 @@
 			fmt.Printf("Could not complete oauth handshake: %v\n", err)
 			return
 		}
+	default:
+		fmt.Printf("Continuing without authentication\n")
 	}
 
 	root, err := NewJiraDir("", 0555|qp.DMDIR, "jira", "jira", client, &JiraView{})
 	if err != nil {
-		fmt.Printf("Could not create JIRA view")
+		fmt.Printf("Could not create JIRA view\n")
 		return
 	}
 
--- a/utils.go
+++ b/utils.go
@@ -1,26 +1,12 @@
 package main
 
 import (
-	"bytes"
-	"crypto/tls"
-	"crypto/x509"
-	"encoding/json"
-	"encoding/pem"
-	"errors"
 	"fmt"
-	"io"
-	"io/ioutil"
-	"log"
-	"net/http"
-	"net/http/httputil"
 	"net/url"
 	"strings"
-	"time"
 
 	"github.com/andygrunwald/go-jira"
 	"github.com/joushou/qp"
-	"github.com/joushou/qptools/fileserver/trees"
-	"github.com/mrjones/oauth"
 )
 
 type SearchResult struct {
@@ -28,28 +14,17 @@
 }
 
 func GetProjects(jc *Client) ([]jira.Project, error) {
-	req, err := jc.NewRequest("GET", "/rest/api/2/project", nil)
-	if err != nil {
-		return nil, fmt.Errorf("could not query JIRA: %v", err)
-	}
-
 	var projects []jira.Project
-	if _, err := jc.Do(req, &projects); err != nil {
-		return nil, fmt.Errorf("could not query JIRA: %v", err)
+	if err := jc.RPC("GET", "/rest/api/2/project", nil, &projects); err != nil {
+		return nil, fmt.Errorf("could not query projects: %v", err)
 	}
-
 	return projects, nil
 }
 
 func GetTypesForProject(jc *Client, project string) ([]string, error) {
-	req, err := jc.NewRequest("GET", "/rest/api/2/issuetype", nil)
-	if err != nil {
-		return nil, fmt.Errorf("could not query JIRA: %v", err)
-	}
-
 	var types []jira.IssueType
-	if _, err := jc.Do(req, &types); err != nil {
-		return nil, fmt.Errorf("could not query JIRA: %v", err)
+	if err := jc.RPC("GET", "/rest/api/2/issuetype", nil, &types); err != nil {
+		return nil, fmt.Errorf("could not query issue types: %v", err)
 	}
 
 	ss := make([]string, len(types))
@@ -61,16 +36,10 @@
 }
 
 func GetKeysForSearch(jc *Client, query string, max int) ([]string, error) {
-	cmd := fmt.Sprintf("/rest/api/2/search?fields=key&maxResults=%d&jql=%s", max, url.QueryEscape(query))
-
-	req, err := jc.NewRequest("GET", cmd, nil)
-	if err != nil {
-		return nil, fmt.Errorf("could not query JIRA: %v", err)
-	}
-
 	var s SearchResult
-	if _, err := jc.Do(req, &s); err != nil {
-		return nil, fmt.Errorf("could not query JIRA: %v", err)
+	url := fmt.Sprintf("/rest/api/2/search?fields=key&maxResults=%d&jql=%s", max, url.QueryEscape(query))
+	if err := jc.RPC("GET", url, nil, &s); err != nil {
+		return nil, fmt.Errorf("could not execute search: %v", err)
 	}
 
 	ss := make([]string, len(s.Issues))
@@ -82,16 +51,10 @@
 }
 
 func GetKeysForNIssues(jc *Client, project string, max int) ([]string, error) {
-	cmd := fmt.Sprintf("/rest/api/2/search?fields=key&maxResults=%d&jql=project=%s", max, project)
-
-	req, err := jc.NewRequest("GET", cmd, nil)
-	if err != nil {
-		return nil, fmt.Errorf("could not query JIRA: %v", err)
-	}
-
 	var s SearchResult
-	if _, err := jc.Do(req, &s); err != nil {
-		return nil, fmt.Errorf("could not query JIRA: %v", err)
+	url := fmt.Sprintf("/rest/api/2/search?fields=key&maxResults=%d&jql=project=%s", max, project)
+	if err := jc.RPC("GET", url, nil, &s); err != nil {
+		return nil, fmt.Errorf("could not execute search: %v", err)
 	}
 
 	ss := make([]string, len(s.Issues))
@@ -107,14 +70,10 @@
 }
 
 func GetIssue(jc *Client, key string) (*jira.Issue, error) {
-	req, err := jc.NewRequest("GET", fmt.Sprintf("/rest/api/2/issue/%s", key), nil)
-	if err != nil {
-		return nil, fmt.Errorf("could not query JIRA: %v", err)
-	}
-
 	var i jira.Issue
-	if _, err = jc.Do(req, &i); err != nil {
-		return nil, fmt.Errorf("could not query JIRA: %v", err)
+	url := fmt.Sprintf("/rest/api/2/issue/%s", key)
+	if err := jc.RPC("GET", url, nil, &i); err != nil {
+		return nil, fmt.Errorf("could not query issue: %v", err)
 	}
 	return &i, nil
 }
@@ -125,39 +84,26 @@
 }
 
 func CreateIssue(jc *Client, issue *jira.Issue) (string, error) {
-	req, err := jc.NewRequest("POST", "/rest/api/2/issue", issue)
-	if err != nil {
-		return "", fmt.Errorf("could not query JIRA: %v", err)
-	}
-
 	var cir CreateIssueResult
-	if _, err = jc.Do(req, &cir); err != nil {
-		return "", fmt.Errorf("could not query JIRA: %v", err)
+	if err := jc.RPC("POST", "/rest/api/2/issue", issue, &cir); err != nil {
+		return "", fmt.Errorf("could not create issue: %v", err)
 	}
 	return cir.Key, nil
 }
 
 func DeleteIssue(jc *Client, issue string) error {
-	req, err := jc.NewRequest("DELETE", fmt.Sprintf("/rest/api/2/issue/%s", issue), nil)
-	if err != nil {
-		return fmt.Errorf("could not query JIRA: %v", err)
+	url := fmt.Sprintf("/rest/api/2/issue/%s", issue)
+	if err := jc.RPC("DELETE", url, nil, nil); err != nil {
+		return fmt.Errorf("could not delete issue: %v", err)
 	}
-
-	if _, err = jc.Do(req, nil); err != nil {
-		return fmt.Errorf("could not query JIRA: %v", err)
-	}
 	return nil
 }
 
 func DeleteIssueLink(jc *Client, issueLinkID string) error {
-	req, err := jc.NewRequest("DELETE", fmt.Sprintf("/rest/api/2/issueLink/%s", issueLinkID), nil)
-	if err != nil {
-		return fmt.Errorf("could not query JIRA: %v", err)
+	url := fmt.Sprintf("/rest/api/2/issueLink/%s", issueLinkID)
+	if err := jc.RPC("DELETE", url, nil, nil); err != nil {
+		return fmt.Errorf("could not delete issue link: %v", err)
 	}
-
-	if _, err = jc.Do(req, nil); err != nil {
-		return fmt.Errorf("could not query JIRA: %v", err)
-	}
 	return nil
 }
 
@@ -174,14 +120,9 @@
 		},
 	}
 
-	req, err := jc.NewRequest("POST", "/rest/api/2/issueLink", issueLink)
-	if err != nil {
-		return fmt.Errorf("could not query JIRA: %v", err)
+	if err := jc.RPC("POST", "/rest/api/2/issueLink", issueLink, nil); err != nil {
+		return fmt.Errorf("could not create issue link: %v", err)
 	}
-
-	if _, err = jc.Do(req, nil); err != nil {
-		return fmt.Errorf("could not query JIRA: %v", err)
-	}
 	return nil
 }
 
@@ -196,16 +137,11 @@
 }
 
 func GetTransitionsForIssue(jc *Client, issue string) ([]Transition, error) {
-	req, err := jc.NewRequest("GET", fmt.Sprintf("/rest/api/2/issue/%s/transitions", issue), nil)
-	if err != nil {
-		return nil, fmt.Errorf("could not query JIRA: %v", err)
-	}
-
 	var tr TransitionResult
-	if _, err = jc.Do(req, &tr); err != nil {
-		return nil, fmt.Errorf("could no query JIRA: %v", err)
+	url := fmt.Sprintf("/rest/api/2/issue/%s/transitions", issue)
+	if err := jc.RPC("GET", url, nil, &tr); err != nil {
+		return nil, fmt.Errorf("could not get transitions: %v", err)
 	}
-
 	return tr.Transitions, nil
 }
 
@@ -232,16 +168,10 @@
 			"id": id,
 		},
 	}
-
-	req, err := jc.NewRequest("POST", fmt.Sprintf("/rest/api/2/issue/%s/transitions", issue), post)
-	if err != nil {
-		return fmt.Errorf("could not query JIRA: %v", err)
+	url := fmt.Sprintf("/rest/api/2/issue/%s/transitions", issue)
+	if err := jc.RPC("POST", url, post, nil); err != nil {
+		return fmt.Errorf("could not transition issue: %v", err)
 	}
-
-	if _, err = jc.Do(req, nil); err != nil {
-		return fmt.Errorf("could not query JIRA: %v", err)
-	}
-
 	return nil
 }
 
@@ -251,7 +181,7 @@
 		field = "issuetype"
 	}
 
-	cmd := fmt.Sprintf("/rest/api/2/issue/%s", issue)
+	url := fmt.Sprintf("/rest/api/2/issue/%s", issue)
 	method := "PUT"
 
 	var value interface{}
@@ -296,18 +226,10 @@
 	default:
 		fields[field] = value
 	}
-	req, err := jc.NewRequest(method, cmd, post)
-	if err != nil {
-		return fmt.Errorf("could not query JIRA: %v", err)
-	}
 
-	b, _ := httputil.DumpRequestOut(req, true)
-	log.Printf("Set field body: \n%s\n", b)
-
-	if _, err = jc.Do(req, nil); err != nil {
-		return fmt.Errorf("could not query JIRA: %v", err)
+	if err := jc.RPC(method, url, post, nil); err != nil {
+		return fmt.Errorf("could not set field for issue: %v", err)
 	}
-
 	return nil
 }
 
@@ -316,14 +238,10 @@
 }
 
 func GetCommentsForIssue(jc *Client, issue string) ([]string, error) {
-	req, err := jc.NewRequest("GET", fmt.Sprintf("/rest/api/2/issue/%s/comment?maxResults=1000", issue), nil)
-	if err != nil {
-		return nil, fmt.Errorf("could not query JIRA: %v", err)
-	}
-
 	var cr CommentResult
-	if _, err := jc.Do(req, &cr); err != nil {
-		return nil, fmt.Errorf("could not query JIRA: %v", err)
+	url := fmt.Sprintf("/rest/api/2/issue/%s/comment?maxResults=1000", issue)
+	if err := jc.RPC("GET", url, nil, &cr); err != nil {
+		return nil, fmt.Errorf("could not get comments: %v", err)
 	}
 
 	var ss []string
@@ -335,16 +253,11 @@
 }
 
 func GetComment(jc *Client, issue, id string) (*jira.Comment, error) {
-	req, err := jc.NewRequest("GET", fmt.Sprintf("/rest/api/2/issue/%s/comment/%s", issue, id), nil)
-	if err != nil {
-		return nil, fmt.Errorf("could not query JIRA: %v", err)
-	}
-
 	var c jira.Comment
-	if _, err := jc.Do(req, &c); err != nil {
-		return nil, fmt.Errorf("could not query JIRA: %v", err)
+	url := fmt.Sprintf("/rest/api/2/issue/%s/comment/%s", issue, id)
+	if err := jc.RPC("GET", url, nil, &c); err != nil {
+		return nil, fmt.Errorf("could not get comment: %v", err)
 	}
-
 	return &c, nil
 }
 
@@ -352,16 +265,10 @@
 	c := jira.Comment{
 		Body: body,
 	}
-
-	req, err := jc.NewRequest("PUT", fmt.Sprintf("/rest/api/2/issue/%s/comment/%s", issue, id), c)
-	if err != nil {
-		return fmt.Errorf("could not query JIRA: %v", err)
+	url := fmt.Sprintf("/rest/api/2/issue/%s/comment/%s", issue, id)
+	if err := jc.RPC("PUT", url, c, nil); err != nil {
+		return fmt.Errorf("could not set comment: %v", err)
 	}
-
-	if _, err = jc.Do(req, nil); err != nil {
-		return fmt.Errorf("could not query JIRA: %v", err)
-	}
-
 	return nil
 }
 
@@ -369,48 +276,21 @@
 	c := jira.Comment{
 		Body: body,
 	}
-
-	req, err := jc.NewRequest("POST", fmt.Sprintf("/rest/api/2/issue/%s/comment/", issue), c)
-	if err != nil {
-		return fmt.Errorf("could not query JIRA: %v", err)
+	url := fmt.Sprintf("/rest/api/2/issue/%s/comment/", issue)
+	if err := jc.RPC("POST", url, c, nil); err != nil {
+		return fmt.Errorf("could not add comment: %v", err)
 	}
-
-	if _, err = jc.Do(req, nil); err != nil {
-		return fmt.Errorf("could not query JIRA: %v", err)
-	}
-
 	return nil
 }
 
 func RemoveComment(jc *Client, issue, id string) error {
-	req, err := jc.NewRequest("DELETE", fmt.Sprintf("/rest/api/2/issue/%s/comment/%s", issue, id), nil)
-	if err != nil {
-		return fmt.Errorf("could not query JIRA: %v", err)
+	url := fmt.Sprintf("/rest/api/2/issue/%s/comment/%s", issue, id)
+	if err := jc.RPC("DELETE", url, nil, nil); err != nil {
+		return fmt.Errorf("could not delete comment: %v", err)
 	}
-
-	if _, err = jc.Do(req, nil); err != nil {
-		return fmt.Errorf("could not query JIRA: %v", err)
-	}
-
 	return nil
 }
 
-type CanOpenAndLister interface {
-	CanOpen(string, qp.OpenMode) bool
-	trees.Lister
-}
-
-func OpenList(l CanOpenAndLister, user string, mode qp.OpenMode) (trees.ReadWriteAtCloser, error) {
-	if !l.CanOpen(user, mode) {
-		return nil, errors.New("permission denied")
-	}
-
-	return &trees.ListHandle{
-		Dir:  l,
-		User: user,
-	}, nil
-}
-
 func StringsToStats(strs []string, Perm qp.FileMode, user, group string) []qp.Stat {
 	var stats []qp.Stat
 	for _, str := range strs {
@@ -437,325 +317,4 @@
 	}
 
 	return false
-}
-
-type CloseSaverHandle struct {
-	onClose func() error
-	trees.ReadWriteAtCloser
-}
-
-func (csh *CloseSaverHandle) Close() error {
-	err := csh.ReadWriteAtCloser.Close()
-	if err != nil {
-		return err
-	}
-
-	if csh.onClose != nil {
-		return csh.onClose()
-	}
-
-	return nil
-}
-
-type CloseSaver struct {
-	onClose func() error
-	trees.File
-}
-
-func (cs *CloseSaver) Open(user string, mode qp.OpenMode) (trees.ReadWriteAtCloser, error) {
-	hndl, err := cs.File.Open(user, mode)
-	if err != nil {
-		return nil, err
-	}
-
-	var closer func() error
-
-	switch mode & 3 {
-	case qp.OWRITE, qp.ORDWR:
-		closer = cs.onClose
-	}
-
-	return &CloseSaverHandle{
-		ReadWriteAtCloser: hndl,
-		onClose:           closer,
-	}, nil
-}
-
-func NewCloseSaver(file trees.File, onClose func() error) trees.File {
-	return &CloseSaver{
-		onClose: onClose,
-		File:    file,
-	}
-}
-
-type CommandFile struct {
-	cmds map[string]func([]string) error
-	*trees.SyntheticFile
-}
-
-func (cf *CommandFile) Close() error { return nil }
-func (cf *CommandFile) ReadAt(p []byte, offset int64) (int, error) {
-	return 0, errors.New("cannot read from command file")
-}
-
-func (cf *CommandFile) WriteAt(p []byte, offset int64) (int, error) {
-	args := strings.Split(strings.Trim(string(p), " \n"), " ")
-	cmd := args[0]
-	args = args[1:]
-
-	if f, exists := cf.cmds[cmd]; exists {
-		err := f(args)
-		if err != nil {
-			log.Printf("Command %s failed: %v", cmd, err)
-		}
-		return len(p), err
-	}
-	return len(p), errors.New("no such command")
-}
-
-func (cf *CommandFile) Open(user string, mode qp.OpenMode) (trees.ReadWriteAtCloser, error) {
-	if !cf.CanOpen(user, mode) {
-		return nil, trees.ErrPermissionDenied
-	}
-
-	return cf, nil
-}
-
-func NewCommandFile(name string, perms qp.FileMode, user, group string, cmds map[string]func([]string) error) *CommandFile {
-	return &CommandFile{
-		cmds:          cmds,
-		SyntheticFile: trees.NewSyntheticFile(name, perms, user, group),
-	}
-}
-
-type JiraDir struct {
-	thing  interface{}
-	client *Client
-	*trees.SyntheticDir
-}
-
-func (jd *JiraDir) Walk(user, name string) (trees.File, error) {
-
-	if f, ok := jd.thing.(jiraWalker); ok {
-		return f.Walk(jd.client, name)
-	}
-	if f, ok := jd.thing.(trees.Dir); ok {
-		return f.Walk(user, name)
-	}
-
-	return nil, trees.ErrPermissionDenied
-}
-
-func (jd *JiraDir) List(user string) ([]qp.Stat, error) {
-	if f, ok := jd.thing.(jiraLister); ok {
-		return f.List(jd.client)
-	}
-	if f, ok := jd.thing.(trees.Lister); ok {
-		return f.List(user)
-	}
-
-	return nil, trees.ErrPermissionDenied
-}
-
-func (jd *JiraDir) Remove(user, name string) error {
-	if f, ok := jd.thing.(jiraRemover); ok {
-		return f.Remove(jd.client, name)
-	}
-	if f, ok := jd.thing.(trees.Dir); ok {
-		return f.Remove(user, name)
-	}
-
-	return trees.ErrPermissionDenied
-}
-
-func (jd *JiraDir) Create(user, name string, perms qp.FileMode) (trees.File, error) {
-	return nil, trees.ErrPermissionDenied
-}
-
-func (jd *JiraDir) Open(user string, mode qp.OpenMode) (trees.ReadWriteAtCloser, error) {
-	if !jd.CanOpen(user, mode) {
-		return nil, errors.New("access denied")
-	}
-
-	jd.Lock()
-	defer jd.Unlock()
-	jd.Atime = time.Now()
-	jd.Opens++
-	return &trees.ListHandle{
-		Dir:  jd,
-		User: user,
-	}, nil
-}
-
-func NewJiraDir(name string, perm qp.FileMode, user, group string, jc *Client, thing interface{}) (*JiraDir, error) {
-	switch thing.(type) {
-	case trees.Dir, jiraWalker, jiraLister, jiraRemover:
-	default:
-		return nil, fmt.Errorf("unsupported type: %T", thing)
-	}
-
-	return &JiraDir{
-		thing:        thing,
-		client:       jc,
-		SyntheticDir: trees.NewSyntheticDir(name, perm, user, group),
-	}, nil
-}
-
-type Client struct {
-	user, pass string
-	jiraURL    *url.URL
-	cookies    []*http.Cookie
-	usingOAuth bool
-	*http.Client
-}
-
-func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) {
-	rel, err := url.Parse(urlStr)
-	if err != nil {
-		return nil, err
-	}
-
-	u := c.jiraURL.ResolveReference(rel)
-
-	var buf io.ReadWriter
-	if body != nil {
-		buf = new(bytes.Buffer)
-		err := json.NewEncoder(buf).Encode(body)
-		if err != nil {
-			return nil, err
-		}
-	}
-
-	req, err := http.NewRequest(method, u.String(), buf)
-	if err != nil {
-		return nil, err
-	}
-
-	req.Header.Set("Content-Type", "application/json")
-
-	// Set session cookie if there is one
-	for _, cookie := range c.cookies {
-		req.AddCookie(cookie)
-	}
-
-	return req, nil
-}
-
-func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) {
-	resp, err := c.Client.Do(req)
-	if err != nil {
-		return nil, err
-	}
-
-	respBody, err := ioutil.ReadAll(resp.Body)
-	if err != nil {
-		return resp, err
-	}
-
-	if !(resp.StatusCode >= 200 && resp.StatusCode <= 299) {
-		return resp, fmt.Errorf("Error (status code %d): %s", resp.StatusCode, respBody)
-	}
-
-	if v != nil {
-		// Open a NewDecoder and defer closing the reader only if there is a provided interface to decode to
-		defer resp.Body.Close()
-		err = json.Unmarshal(respBody, v)
-	}
-
-	return resp, err
-}
-
-func (c *Client) AcquireSessionCookie(username, password string) (bool, error) {
-	apiEndpoint := "rest/auth/1/session"
-	body := struct {
-		Username string `json:"username"`
-		Password string `json:"password"`
-	}{
-		username,
-		password,
-	}
-
-	req, err := c.NewRequest("POST", apiEndpoint, body)
-	if err != nil {
-		return false, err
-	}
-
-	resp, err := c.Do(req, nil)
-	c.cookies = resp.Cookies()
-
-	if err != nil {
-		return false, fmt.Errorf("Auth at JIRA instance failed (HTTP(S) request). %s", err)
-	}
-	if resp != nil && resp.StatusCode != 200 {
-		return false, fmt.Errorf("Auth at JIRA instance failed (HTTP(S) request). Status code: %d", resp.StatusCode)
-	}
-
-	return true, nil
-}
-
-func (c *Client) login() error {
-	if c.usingOAuth {
-		return nil
-	}
-	res, err := c.AcquireSessionCookie(c.user, c.pass)
-	if err != nil || res == false {
-		return fmt.Errorf("Could not authenticate to JIRA: %v\n", err)
-	}
-	return nil
-}
-
-func (c *Client) oauth(consumerKey, privateKeyFile string) error {
-	pvf, err := ioutil.ReadFile(privateKeyFile)
-	if err != nil {
-		return err
-	}
-
-	block, _ := pem.Decode(pvf)
-	privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
-	if err != nil {
-		return err
-	}
-
-	url1, _ := c.jiraURL.Parse("/plugins/servlet/oauth/request-token")
-	url2, _ := c.jiraURL.Parse("/plugins/servlet/oauth/authorize")
-	url3, _ := c.jiraURL.Parse("/plugins/servlet/oauth/access-token")
-
-	t := oauth.NewRSAConsumer(
-		consumerKey,
-		privateKey,
-		oauth.ServiceProvider{
-			RequestTokenUrl:   url1.String(),
-			AuthorizeTokenUrl: url2.String(),
-			AccessTokenUrl:    url3.String(),
-			HttpMethod:        "POST",
-		},
-	)
-
-	t.HttpClient = &http.Client{
-		Transport: &http.Transport{
-			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
-		},
-	}
-
-	requestToken, url, err := t.GetRequestTokenAndUrl("oob")
-	if err != nil {
-		return err
-	}
-
-	fmt.Printf("OAuth token requested. Please to go the following URL:\n\t%s\n\nEnter verification code: ", url)
-	var verificationCode string
-	fmt.Scanln(&verificationCode)
-	accessToken, err := t.AuthorizeToken(requestToken, verificationCode)
-	if err != nil {
-		return err
-	}
-	fmt.Printf("OAuth token authorized.\n")
-
-	client, err := t.MakeHttpClient(accessToken)
-	if err != nil {
-		return err
-	}
-
-	c.Client = client
-	return nil
 }
--- a/workflow.go
+++ b/workflow.go
@@ -12,24 +12,16 @@
 }
 
 func BuildWorkflow1(jc *Client, project, issueTypeNo string) (*WorkflowGraph, error) {
-	req, err := jc.NewRequest("GET", fmt.Sprintf("/rest/projectconfig/latest/issuetype/%s/%s/workflow", project, issueTypeNo), nil)
-	if err != nil {
-		return nil, fmt.Errorf("could not query JIRA 1: %v", err)
-	}
-
 	var t thing
-	if _, err = jc.Do(req, &t); err != nil {
-		return nil, fmt.Errorf("could not query JIRA 2: %v", err)
+	u := fmt.Sprintf("/rest/projectconfig/latest/issuetype/%s/%s/workflow", project, issueTypeNo)
+	if err := jc.RPC("GET", u, nil, &t); err != nil {
+		return nil, fmt.Errorf("could not query workflow for issue: %v", err)
 	}
 
-	req, err = jc.NewRequest("GET", fmt.Sprintf("/rest/projectconfig/latest/workflow?workflowName=%s", url.QueryEscape(t.Name)), nil)
-	if err != nil {
-		return nil, fmt.Errorf("could not query JIRA 3: %v", err)
-	}
-
 	var wr WorkflowResponse1
-	if _, err = jc.Do(req, &wr); err != nil {
-		return nil, fmt.Errorf("could not query JIRA 4: %v", err)
+	u = fmt.Sprintf("/rest/projectconfig/latest/workflow?workflowName=%s", url.QueryEscape(t.Name))
+	if err := jc.RPC("GET", u, nil, &wr); err != nil {
+		return nil, fmt.Errorf("could not query workflow graph: %v", err)
 	}
 
 	var wg WorkflowGraph
@@ -38,26 +30,16 @@
 }
 
 func BuildWorkflow2(jc *Client, project, issueTypeNo string) (*WorkflowGraph, error) {
-	req, err := jc.NewRequest("GET", fmt.Sprintf("/rest/projectconfig/latest/issuetype/%s/%s/workflow", project, issueTypeNo), nil)
-	if err != nil {
-		return nil, fmt.Errorf("could not query JIRA 1: %v", err)
-	}
-
 	var t thing
-	if _, err = jc.Do(req, &t); err != nil {
-		return nil, fmt.Errorf("could not query JIRA 2: %v", err)
+	u := fmt.Sprintf("/rest/projectconfig/latest/issuetype/%s/%s/workflow", project, issueTypeNo)
+	if err := jc.RPC("GET", u, nil, &t); err != nil {
+		return nil, fmt.Errorf("could not query workflow for issue: %v", err)
 	}
 
-	req, err = jc.NewRequest("GET", fmt.Sprintf("/rest/workflowDesigner/latest/workflows?name=%s", url.QueryEscape(t.Name)), nil)
-	if err != nil {
-		return nil, fmt.Errorf("could not query JIRA 3: %v", err)
-	}
-
-	req.Header.Set("X-Atlassian-Token", "nocheck")
-
 	var wr WorkflowResponse2
-	if _, err = jc.Do(req, &wr); err != nil {
-		return nil, fmt.Errorf("could not query JIRA 4: %v", err)
+	u = fmt.Sprintf("/rest/workflowDesigner/latest/workflows?name=%s", url.QueryEscape(t.Name))
+	if err := jc.RPC("GET", u, nil, &wr); err != nil {
+		return nil, fmt.Errorf("could not query workflow graph: %v", err)
 	}
 
 	var wg WorkflowGraph