<commit_before>package main

import (
	"crypto/aes"
	"crypto/cipher"
	"encoding/base64"
	"encoding/json"
	"flag"
	"fmt"
	"log"
	"net/http"
	"strings"

	"github.com/sironfoot/go-twitter-bot/data/api"
	"github.com/sironfoot/go-twitter-bot/data/db"
	"github.com/sironfoot/go-twitter-bot/lib/config"

	"goji.io"
	"goji.io/middleware"
	"goji.io/pat"
	"golang.org/x/net/context"
)

func main() {
	var configuration api.Config
	err := config.Load("config.json", "dev", &configuration)
	if err != nil {
		log.Fatal(err)
	}

	addr := flag.String("addr", configuration.AppSettings.ServerAddress, "Address to run server on")
	dbConn := flag.String("db", configuration.Database.ConnectionString, "Database connection string")
	flag.Parse()

	err = db.InitDB(*dbConn)
	if err != nil {
		log.Fatal(err)
	}

	router := goji.NewMux()

	// 1. Error handling
	router.UseC(func(next goji.Handler) goji.Handler {
		return goji.HandlerFunc(func(ctx context.Context, res http.ResponseWriter, req *http.Request) {
			defer func() {
				r := recover()
				if r != nil {
					log.Println(r)

					response := api.MessageResponse{
						Message: "Internal Server Error",
					}

					res.Header().Set("Content-Type", "application/json; charset=utf-8")
					res.WriteHeader(http.StatusInternalServerError)

					data, jsonErr := json.MarshalIndent(response, "", "    ")
					if jsonErr == nil {
						res.Write(data)
					}
				}
			}()

			next.ServeHTTPC(ctx, res, req)
		})
	})

	// 2. Setup request context values
	router.UseC(func(next goji.Handler) goji.Handler {
		return goji.HandlerFunc(func(ctx context.Context, res http.ResponseWriter, req *http.Request) {
			res.Header().Set("Content-Type", "application/json; charset=utf-8")

			appContext := api.AppContext{}
			appContext.Settings = configuration
			ctx = context.WithValue(ctx, "appContext", &appContext)

			next.ServeHTTPC(ctx, res, req)
		})
	})

	// 3. Check authorisation/logged in state
	router.UseC(func(next goji.Handler) goji.Handler {
		return goji.HandlerFunc(func(ctx context.Context, res http.ResponseWriter, req *http.Request) {
			defer func() {
				next.ServeHTTPC(ctx, res, req)
			}()

			accessToken := req.Header.Get("accessToken")
			if len(accessToken) == 0 {
				return
			}

			encryptedToken, err := base64.StdEncoding.DecodeString(accessToken)
			if err != nil {
				// not a valid Base64 string, force user to log in again
				return
			}

			appContext := ctx.Value("appContext").(*api.AppContext)
			block, err := aes.NewCipher([]byte(appContext.Settings.AppSettings.EncryptionKey))
			if err != nil {
				panic(err)
			}

			if len(encryptedToken) < aes.BlockSize {
				panic(fmt.Errorf("encryptedToken too short"))
			}

			iv := encryptedToken[:aes.BlockSize]
			encryptedToken = encryptedToken[aes.BlockSize:]

			cfb := cipher.NewCFBDecrypter(block, iv)
			cfb.XORKeyStream(encryptedToken, encryptedToken)

			token := string(encryptedToken)
			tokenParts := strings.Split(token, "_")
			if len(tokenParts) != 2 {
				// not a valid format (UserID_AuthToken), force user to log in again
				return
			}

			userID := tokenParts[0]
			authToken := tokenParts[1]

			user, err := db.UserFromID(userID)
			if err != nil {
				panic(err)
			}

			if user.AuthToken.Valid && user.AuthToken.String == authToken {
				appContext.AuthUser = &user
			}
		})
	})

	// 4. Check for 404
	var notFoundHandler = func(next goji.Handler) goji.Handler {
		return goji.HandlerFunc(func(ctx context.Context, res http.ResponseWriter, req *http.Request) {
			if middleware.Handler(ctx) == nil {
				response := api.MessageResponse{
					Message: "Page Not Found",
				}

				res.WriteHeader(http.StatusNotFound)

				data, jsonErr := json.MarshalIndent(response, "", "    ")
				if jsonErr != nil {
					panic(jsonErr)
				}
				res.Write(data)
			} else {
				next.ServeHTTPC(ctx, res, req)
			}
		})
	}

	router.UseC(notFoundHandler)

	// 5. Serve response as JSON
	router.UseC(func(next goji.Handler) goji.Handler {
		return goji.HandlerFunc(func(ctx context.Context, res http.ResponseWriter, req *http.Request) {
			next.ServeHTTPC(ctx, res, req)

			response := ctx.Value("appContext").(*api.AppContext).Response

			if response != nil {
				data, jsonErr := json.MarshalIndent(response, "", "    ")
				if jsonErr != nil {
					panic(jsonErr)
				}
				res.Write(data)
			}
		})
	})

	var mustBeLoggedIn = func(next goji.Handler) goji.Handler {
		return goji.HandlerFunc(func(ctx context.Context, res http.ResponseWriter, req *http.Request) {
			appContext := ctx.Value("appContext").(*api.AppContext)

			if appContext.AuthUser == nil {
				res.WriteHeader(http.StatusUnauthorized)

				response := api.MessageResponse{
					Message: "Not authenticated. Please authenticate with PUT: /account/login",
				}
				data, jsonErr := json.MarshalIndent(response, "", "    ")
				if jsonErr != nil {
					panic(jsonErr)
				}
				res.Write(data)
			} else {
				next.ServeHTTPC(ctx, res, req)
			}
		})
	}

	router.HandleFuncC(pat.Get("/"), func(ctx context.Context, res http.ResponseWriter, req *http.Request) {
		appContext := ctx.Value("appContext").(*api.AppContext)

		appContext.Response = api.MessageResponse{
			Message: "Hello from GoBot Data server",
		}
	})

	// Account
	account := goji.SubMux()
	account.UseC(notFoundHandler)
	router.HandleC(pat.New("/account/*"), account)

	account.HandleFuncC(pat.Put("/login"), api.AccountLogin)
	account.HandleFuncC(pat.Put("/logout"), api.AccountLogout)
	account.HandleFuncC(pat.Post("/signup"), api.AccountSignup)

	// Users
	users := goji.SubMux()
	users.UseC(notFoundHandler)
	users.UseC(mustBeLoggedIn)
	router.HandleC(pat.New("/users/*"), users)
	router.HandleC(pat.New("/users"), users)

	users.HandleFuncC(pat.Get(""), api.UsersAll)
	users.HandleFuncC(pat.Post(""), api.UserCreate)
	users.HandleFuncC(pat.Get("/:userID"), api.UserGet)
	users.HandleFuncC(pat.Put("/:userID"), api.UserUpdate)
	users.HandleFuncC(pat.Delete("/:userID"), api.UserDelete)

	// TwitterAccounts
	twitterAccounts := goji.SubMux()
	twitterAccounts.UseC(notFoundHandler)
	twitterAccounts.UseC(mustBeLoggedIn)
	router.HandleC(pat.New("/twitterAccounts/*"), twitterAccounts)
	router.HandleC(pat.New("/twitterAccounts"), twitterAccounts)

	twitterAccounts.HandleFuncC(pat.Get(""), api.TwitterAccountsAll)
	twitterAccounts.HandleFuncC(pat.Get("/:twitterAccountID"), api.TwitterAccountGet)

	twitterAccounts.HandleFuncC(pat.Get("/:twitterAccountID/tweets"), api.TwitterAccountGetWithTweets)
	twitterAccounts.HandleFuncC(pat.Post("/:twitterAccountID/tweets"), api.TwitterAccountTweetCreate)
	twitterAccounts.HandleFuncC(pat.Put("/:twitterAccountID/tweets/:tweetID"), api.TwitterAccountTweetUpdate)
	twitterAccounts.HandleFuncC(pat.Delete("/:twitterAccountID/tweets/:tweetID"), api.TwitterAccountTweetDelete)

	server := http.Server{
		Addr:    *addr,
		Handler: router,
	}

	log.Printf("GoBot Data Server running on %s...\n", *addr)
	log.Fatal(server.ListenAndServe())
}
<commit_msg>changed: use caching support for loading JSON config oer request.<commit_after>package main

import (
	"crypto/aes"
	"crypto/cipher"
	"encoding/base64"
	"encoding/json"
	"flag"
	"fmt"
	"log"
	"net/http"
	"strings"

	"github.com/sironfoot/go-twitter-bot/data/api"
	"github.com/sironfoot/go-twitter-bot/data/db"
	"github.com/sironfoot/go-twitter-bot/lib/config"

	"goji.io"
	"goji.io/middleware"
	"goji.io/pat"
	"golang.org/x/net/context"
)

func main() {
	var configuration api.Config
	err := config.LoadWithCaching("config.json", "dev", &configuration)
	if err != nil {
		log.Fatal(err)
	}

	addr := flag.String("addr", configuration.AppSettings.ServerAddress, "Address to run server on")
	dbConn := flag.String("db", configuration.Database.ConnectionString, "Database connection string")
	flag.Parse()

	err = db.InitDB(*dbConn)
	if err != nil {
		log.Fatal(err)
	}

	router := goji.NewMux()

	// 1. Error handling
	router.UseC(func(next goji.Handler) goji.Handler {
		return goji.HandlerFunc(func(ctx context.Context, res http.ResponseWriter, req *http.Request) {
			defer func() {
				r := recover()
				if r != nil {
					log.Println(r)

					response := api.MessageResponse{
						Message: "Internal Server Error",
					}

					res.Header().Set("Content-Type", "application/json; charset=utf-8")
					res.WriteHeader(http.StatusInternalServerError)

					data, jsonErr := json.MarshalIndent(response, "", "    ")
					if jsonErr == nil {
						res.Write(data)
					}
				}
			}()

			next.ServeHTTPC(ctx, res, req)
		})
	})

	// 2. Setup request context values
	router.UseC(func(next goji.Handler) goji.Handler {
		return goji.HandlerFunc(func(ctx context.Context, res http.ResponseWriter, req *http.Request) {
			res.Header().Set("Content-Type", "application/json; charset=utf-8")

			appContext := api.AppContext{}

			var configuration api.Config
			err := config.LoadWithCaching("config.json", "dev", &configuration)
			if err != nil {
				panic(err)
			}

			appContext.Settings = configuration
			ctx = context.WithValue(ctx, "appContext", &appContext)

			next.ServeHTTPC(ctx, res, req)
		})
	})

	// 3. Check authorisation/logged in state
	router.UseC(func(next goji.Handler) goji.Handler {
		return goji.HandlerFunc(func(ctx context.Context, res http.ResponseWriter, req *http.Request) {
			defer func() {
				next.ServeHTTPC(ctx, res, req)
			}()

			accessToken := req.Header.Get("accessToken")
			if len(accessToken) == 0 {
				return
			}

			encryptedToken, err := base64.StdEncoding.DecodeString(accessToken)
			if err != nil {
				// not a valid Base64 string, force user to log in again
				return
			}

			appContext := ctx.Value("appContext").(*api.AppContext)
			block, err := aes.NewCipher([]byte(appContext.Settings.AppSettings.EncryptionKey))
			if err != nil {
				panic(err)
			}

			if len(encryptedToken) < aes.BlockSize {
				panic(fmt.Errorf("encryptedToken too short"))
			}

			iv := encryptedToken[:aes.BlockSize]
			encryptedToken = encryptedToken[aes.BlockSize:]

			cfb := cipher.NewCFBDecrypter(block, iv)
			cfb.XORKeyStream(encryptedToken, encryptedToken)

			token := string(encryptedToken)
			tokenParts := strings.Split(token, "_")
			if len(tokenParts) != 2 {
				// not a valid format (UserID_AuthToken), force user to log in again
				return
			}

			userID := tokenParts[0]
			authToken := tokenParts[1]

			user, err := db.UserFromID(userID)
			if err != nil {
				panic(err)
			}

			if user.AuthToken.Valid && user.AuthToken.String == authToken {
				appContext.AuthUser = &user
			}
		})
	})

	// 4. Check for 404
	var notFoundHandler = func(next goji.Handler) goji.Handler {
		return goji.HandlerFunc(func(ctx context.Context, res http.ResponseWriter, req *http.Request) {
			if middleware.Handler(ctx) == nil {
				response := api.MessageResponse{
					Message: "Page Not Found",
				}

				res.WriteHeader(http.StatusNotFound)

				data, jsonErr := json.MarshalIndent(response, "", "    ")
				if jsonErr != nil {
					panic(jsonErr)
				}
				res.Write(data)
			} else {
				next.ServeHTTPC(ctx, res, req)
			}
		})
	}

	router.UseC(notFoundHandler)

	// 5. Serve response as JSON
	router.UseC(func(next goji.Handler) goji.Handler {
		return goji.HandlerFunc(func(ctx context.Context, res http.ResponseWriter, req *http.Request) {
			next.ServeHTTPC(ctx, res, req)

			response := ctx.Value("appContext").(*api.AppContext).Response

			if response != nil {
				data, jsonErr := json.MarshalIndent(response, "", "    ")
				if jsonErr != nil {
					panic(jsonErr)
				}
				res.Write(data)
			}
		})
	})

	var mustBeLoggedIn = func(next goji.Handler) goji.Handler {
		return goji.HandlerFunc(func(ctx context.Context, res http.ResponseWriter, req *http.Request) {
			appContext := ctx.Value("appContext").(*api.AppContext)

			if appContext.AuthUser == nil {
				res.WriteHeader(http.StatusUnauthorized)

				response := api.MessageResponse{
					Message: "Not authenticated. Please authenticate with PUT: /account/login",
				}
				data, jsonErr := json.MarshalIndent(response, "", "    ")
				if jsonErr != nil {
					panic(jsonErr)
				}
				res.Write(data)
			} else {
				next.ServeHTTPC(ctx, res, req)
			}
		})
	}

	router.HandleFuncC(pat.Get("/"), func(ctx context.Context, res http.ResponseWriter, req *http.Request) {
		appContext := ctx.Value("appContext").(*api.AppContext)

		appContext.Response = api.MessageResponse{
			Message: "Hello from GoBot Data server",
		}
	})

	// Account
	account := goji.SubMux()
	account.UseC(notFoundHandler)
	router.HandleC(pat.New("/account/*"), account)

	account.HandleFuncC(pat.Put("/login"), api.AccountLogin)
	account.HandleFuncC(pat.Put("/logout"), api.AccountLogout)
	account.HandleFuncC(pat.Post("/signup"), api.AccountSignup)

	// Users
	users := goji.SubMux()
	users.UseC(notFoundHandler)
	users.UseC(mustBeLoggedIn)
	router.HandleC(pat.New("/users/*"), users)
	router.HandleC(pat.New("/users"), users)

	users.HandleFuncC(pat.Get(""), api.UsersAll)
	users.HandleFuncC(pat.Post(""), api.UserCreate)
	users.HandleFuncC(pat.Get("/:userID"), api.UserGet)
	users.HandleFuncC(pat.Put("/:userID"), api.UserUpdate)
	users.HandleFuncC(pat.Delete("/:userID"), api.UserDelete)

	// TwitterAccounts
	twitterAccounts := goji.SubMux()
	twitterAccounts.UseC(notFoundHandler)
	twitterAccounts.UseC(mustBeLoggedIn)
	router.HandleC(pat.New("/twitterAccounts/*"), twitterAccounts)
	router.HandleC(pat.New("/twitterAccounts"), twitterAccounts)

	twitterAccounts.HandleFuncC(pat.Get(""), api.TwitterAccountsAll)
	twitterAccounts.HandleFuncC(pat.Get("/:twitterAccountID"), api.TwitterAccountGet)

	twitterAccounts.HandleFuncC(pat.Get("/:twitterAccountID/tweets"), api.TwitterAccountGetWithTweets)
	twitterAccounts.HandleFuncC(pat.Post("/:twitterAccountID/tweets"), api.TwitterAccountTweetCreate)
	twitterAccounts.HandleFuncC(pat.Put("/:twitterAccountID/tweets/:tweetID"), api.TwitterAccountTweetUpdate)
	twitterAccounts.HandleFuncC(pat.Delete("/:twitterAccountID/tweets/:tweetID"), api.TwitterAccountTweetDelete)

	server := http.Server{
		Addr:    *addr,
		Handler: router,
	}

	log.Printf("GoBot Data Server running on %s...\n", *addr)
	log.Fatal(server.ListenAndServe())
}
<|endoftext|>


<commit_before>package web

import (
	"context"
	"crypto/subtle"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"time"

	"golang.org/x/oauth2"

	"github.com/gorilla/securecookie"
	"github.com/gorilla/sessions"
	"github.com/psimika/secure-web-app/petfind"
)

type contextKey int

const (
	userContextKey contextKey = iota
	sessionIDContextKey
)

// auth protects other handlers letting only logged in users access them. If
// the session ID is not found in the context, the handler redirects to /login.
func (s *server) auth(fn handler) handler {
	return func(w http.ResponseWriter, r *http.Request) *Error {
		log.Println("r.Referer():", r.Referer())
		log.Println("r.RemoteAddr:", r.RemoteAddr)
		log.Println("r.Header.Get(\"X-Forwarded-For\"):", r.Header.Get("X-Forwarded-For"))

		session, err := s.sessions.Get(r, sessionName)
		if err != nil {
			log.Printf("error getting session: %v", err)
			http.Redirect(w, r, "/login", http.StatusFound)
			return nil
		}

		// Get the session's created value to find when it was created.
		t, ok := session.Values["created"]
		if !ok {
			log.Println("session has no created value")
			http.Redirect(w, r, "/login", http.StatusFound)
			return nil
		}
		created, ok := t.(int64)
		if !ok {
			log.Println("unexpected created type")
			http.Redirect(w, r, "/login", http.StatusFound)
			return nil
		}

		// If the session has expired then we delete it.
		expirationTime := time.Unix(created, 0).Add(time.Duration(s.sessionMaxTTL) * time.Second)
		if expirationTime.Before(time.Now()) {
			log.Println("session expired")
			session.Options.MaxAge = -1
			if err = sessions.Save(r, w); err != nil {
				return E(err, "error deleting expired session", http.StatusInternalServerError)
			}
			http.Redirect(w, r, "/login", http.StatusFound)
			return nil
		}

		// Get the user's ID stored in the session.
		v, ok := session.Values["userID"]
		if !ok {
			log.Println("session has no userID")
			http.Redirect(w, r, "/login", http.StatusFound)
			return nil
		}
		userID, ok := v.(int64)
		if !ok {
			log.Println("unexpected userID type")
			http.Redirect(w, r, "/login", http.StatusFound)
			return nil
		}

		// Get the user from the database based on the session's user ID.
		user, err := s.store.GetUser(userID)
		if err != nil {
			return E(err, "error getting user", http.StatusInternalServerError)
		}

		// Extend session's idle timeout.
		session.Options.MaxAge = s.sessionTTL
		if err = sessions.Save(r, w); err != nil {
			return E(err, "error saving session", http.StatusInternalServerError)
		}

		// Put user in the context so that the next handler can access it.
		ctx := newContextWithUser(r.Context(), user)
		fn(w, r.WithContext(ctx))
		return nil
	}
}

// ---
// Helper functions to add and retrieve details of the logged in user from the
// context (Ajmani 2014).
//
// https://blog.golang.org/context

// fromContextGetUser retrieves User from the context.
func fromContextGetUser(ctx context.Context) (*petfind.User, bool) {
	user, ok := ctx.Value(userContextKey).(*petfind.User)
	return user, ok
}

// newContextWithUser adds User to the context.
func newContextWithUser(ctx context.Context, user *petfind.User) context.Context {
	return context.WithValue(ctx, userContextKey, user)
}

// fromContextGetSessionID retrieves sessionID from the context.
func fromContextGetSessionID(ctx context.Context) (string, bool) {
	sessionID, ok := ctx.Value(sessionIDContextKey).(string)
	return sessionID, ok
}

// newContextWithSessionID adds sessionID to the context.
func newContextWithSessionID(ctx context.Context, sessionID string) context.Context {
	return context.WithValue(ctx, sessionIDContextKey, sessionID)
}

// ---

const (
	// OWASP (2017b) recommends using a generic name such as "id" for the
	// session ID name to avoid exposing implementation details:
	//
	// https://www.owasp.org/index.php/Session_Management_Cheat_Sheet#Session_ID_Name_Fingerprinting
	sessionName         = "id"
	oauthStateTokenSize = 32
)

func (s *server) serveLogin(w http.ResponseWriter, r *http.Request) *Error {
	if err := s.tmpl.login.Execute(w, nil); err != nil {
		return E(err, "error rendering login", http.StatusInternalServerError)
	}
	return nil
}

func (s *server) handleLogout(w http.ResponseWriter, r *http.Request) *Error {
	if r.Method != "POST" {
		return E(nil, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
	}

	session, err := s.sessions.Get(r, sessionName)
	if err != nil {
		return E(err, "error getting session for logout", http.StatusInternalServerError)
	}
	session.Options.MaxAge = -1
	if err = sessions.Save(r, w); err != nil {
		return E(err, "error deleting session for logout", http.StatusInternalServerError)
	}

	http.Redirect(w, r, "/", http.StatusFound)
	return nil
}

type GitHubUser struct {
	ID    int64  `json:"id"`
	Login string `json:"login"`
	Name  string `json:"name"`
	Email string `json:"email"`
}

// handleLoginGitHub sends an oauth request to login with GitHub (Caserta
// 2015).
//
// http://pierrecaserta.com/go-oauth-facebook-github-twitter-google-plus/
func (s *server) handleLoginGitHub(w http.ResponseWriter, r *http.Request) *Error {
	// Create oauth state token.
	key := securecookie.GenerateRandomKey(oauthStateTokenSize)
	if key == nil {
		return E(nil, "error generating random oauth state token", http.StatusInternalServerError)
	}
	state := base64.URLEncoding.EncodeToString(key)

	session, err := s.sessions.Get(r, sessionName)
	if err != nil {
		return E(err, "error getting session", http.StatusInternalServerError)
	}
	session.Values["state"] = state
	session.Values["created"] = time.Now().UTC().Unix()
	if err := session.Save(r, w); err != nil {
		return E(err, "error saving session", http.StatusInternalServerError)
	}

	url := s.github.AuthCodeURL(state, oauth2.AccessTypeOnline)
	http.Redirect(w, r, url, http.StatusTemporaryRedirect)
	return nil
}

// handleLoginGitHubCallback receives the callback request returned by GitHub
// after the user has given consent to access their information (Caserta 2015):
//
// http://pierrecaserta.com/go-oauth-facebook-github-twitter-google-plus/
func (s *server) handleLoginGitHubCallback(w http.ResponseWriter, r *http.Request) *Error {
	session, err := s.sessions.Get(r, sessionName)
	if err != nil {
		return E(err, "error getting session", http.StatusInternalServerError)
	}
	val, ok := session.Values["state"]
	if !ok {
		return E(nil, "no state in session", http.StatusForbidden)
	}
	oauthState, ok := val.(string)
	if !ok {
		return E(nil, "unexpected state type", http.StatusForbidden)
	}

	// Check that the state token returned from GitHub is the same as the one
	// we generated.
	state := r.FormValue("state")
	// Constant time compare to mitigate timing attacks.
	if subtle.ConstantTimeCompare([]byte(state), []byte(oauthState)) != 1 {
		// TODO(psimika): Redirect instead of returning an error?
		e := fmt.Errorf("invalid oauth state, expected %q, got %q\n", oauthState, state)
		return E(e, "invalid oauth state", http.StatusForbidden)
	}

	// Exchange authorization code for a GitHub API token.
	code := r.FormValue("code")
	token, err := s.github.Exchange(oauth2.NoContext, code)
	if err != nil {
		return E(err, "error exchanging authorization code for token", http.StatusInternalServerError)
	}

	// Create an HTTP client that uses the GitHub API token.
	c := s.github.Client(oauth2.NoContext, token)

	// Use the client to get the consented user's info from the GitHub API.
	githubUser, err := getGitHubUser(c)
	if err != nil {
		return E(err, "could not get user from GitHub API", http.StatusInternalServerError)
	}

	user, err := s.store.GetUserByGithubID(githubUser.ID)
	// If the user is not found in our database we create them.
	if err != nil && err == petfind.ErrNotFound {
		user = &petfind.User{
			Name:     githubUser.Name,
			GithubID: githubUser.ID,
			Login:    githubUser.Login,
		}
		if cerr := s.store.CreateUser(user); cerr != nil {
			return E(cerr, "could not create user", http.StatusInternalServerError)
		}
	}
	if err != nil && err != petfind.ErrNotFound {
		return E(err, "could not retrieve user", http.StatusInternalServerError)
	}

	session.Values["userID"] = user.ID
	delete(session.Values, "state")
	if err := session.Save(r, w); err != nil {
		return E(err, "error saving session", http.StatusInternalServerError)
	}
	return nil
}

func getGitHubUser(client *http.Client) (*GitHubUser, error) {
	resp, err := client.Get("https://api.github.com/user")
	if err != nil {
		return nil, err
	}
	user := new(GitHubUser)
	if err := json.NewDecoder(resp.Body).Decode(user); err != nil {
		return nil, fmt.Errorf("could not decode user: %v", err)
	}
	return user, nil
}
<commit_msg>Change deprecated oauth2.NoContext<commit_after>package web

import (
	"context"
	"crypto/subtle"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"time"

	"golang.org/x/oauth2"

	"github.com/gorilla/securecookie"
	"github.com/gorilla/sessions"
	"github.com/psimika/secure-web-app/petfind"
)

type contextKey int

const (
	userContextKey contextKey = iota
	sessionIDContextKey
)

// auth protects other handlers letting only logged in users access them. If
// the session ID is not found in the context, the handler redirects to /login.
func (s *server) auth(fn handler) handler {
	return func(w http.ResponseWriter, r *http.Request) *Error {
		log.Println("r.Referer():", r.Referer())
		log.Println("r.RemoteAddr:", r.RemoteAddr)
		log.Println("r.Header.Get(\"X-Forwarded-For\"):", r.Header.Get("X-Forwarded-For"))

		session, err := s.sessions.Get(r, sessionName)
		if err != nil {
			log.Printf("error getting session: %v", err)
			http.Redirect(w, r, "/login", http.StatusFound)
			return nil
		}

		// Get the session's created value to find when it was created.
		t, ok := session.Values["created"]
		if !ok {
			log.Println("session has no created value")
			http.Redirect(w, r, "/login", http.StatusFound)
			return nil
		}
		created, ok := t.(int64)
		if !ok {
			log.Println("unexpected created type")
			http.Redirect(w, r, "/login", http.StatusFound)
			return nil
		}

		// If the session has expired then we delete it.
		expirationTime := time.Unix(created, 0).Add(time.Duration(s.sessionMaxTTL) * time.Second)
		if expirationTime.Before(time.Now()) {
			log.Println("session expired")
			session.Options.MaxAge = -1
			if err = sessions.Save(r, w); err != nil {
				return E(err, "error deleting expired session", http.StatusInternalServerError)
			}
			http.Redirect(w, r, "/login", http.StatusFound)
			return nil
		}

		// Get the user's ID stored in the session.
		v, ok := session.Values["userID"]
		if !ok {
			log.Println("session has no userID")
			http.Redirect(w, r, "/login", http.StatusFound)
			return nil
		}
		userID, ok := v.(int64)
		if !ok {
			log.Println("unexpected userID type")
			http.Redirect(w, r, "/login", http.StatusFound)
			return nil
		}

		// Get the user from the database based on the session's user ID.
		user, err := s.store.GetUser(userID)
		if err != nil {
			return E(err, "error getting user", http.StatusInternalServerError)
		}

		// Extend session's idle timeout.
		session.Options.MaxAge = s.sessionTTL
		if err = sessions.Save(r, w); err != nil {
			return E(err, "error saving session", http.StatusInternalServerError)
		}

		// Put user in the context so that the next handler can access it.
		ctx := newContextWithUser(r.Context(), user)
		fn(w, r.WithContext(ctx))
		return nil
	}
}

// ---
// Helper functions to add and retrieve details of the logged in user from the
// context (Ajmani 2014).
//
// https://blog.golang.org/context

// fromContextGetUser retrieves User from the context.
func fromContextGetUser(ctx context.Context) (*petfind.User, bool) {
	user, ok := ctx.Value(userContextKey).(*petfind.User)
	return user, ok
}

// newContextWithUser adds User to the context.
func newContextWithUser(ctx context.Context, user *petfind.User) context.Context {
	return context.WithValue(ctx, userContextKey, user)
}

// fromContextGetSessionID retrieves sessionID from the context.
func fromContextGetSessionID(ctx context.Context) (string, bool) {
	sessionID, ok := ctx.Value(sessionIDContextKey).(string)
	return sessionID, ok
}

// newContextWithSessionID adds sessionID to the context.
func newContextWithSessionID(ctx context.Context, sessionID string) context.Context {
	return context.WithValue(ctx, sessionIDContextKey, sessionID)
}

// ---

const (
	// OWASP (2017b) recommends using a generic name such as "id" for the
	// session ID name to avoid exposing implementation details:
	//
	// https://www.owasp.org/index.php/Session_Management_Cheat_Sheet#Session_ID_Name_Fingerprinting
	sessionName         = "id"
	oauthStateTokenSize = 32
)

func (s *server) serveLogin(w http.ResponseWriter, r *http.Request) *Error {
	if err := s.tmpl.login.Execute(w, nil); err != nil {
		return E(err, "error rendering login", http.StatusInternalServerError)
	}
	return nil
}

func (s *server) handleLogout(w http.ResponseWriter, r *http.Request) *Error {
	if r.Method != "POST" {
		return E(nil, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
	}

	session, err := s.sessions.Get(r, sessionName)
	if err != nil {
		return E(err, "error getting session for logout", http.StatusInternalServerError)
	}
	session.Options.MaxAge = -1
	if err = sessions.Save(r, w); err != nil {
		return E(err, "error deleting session for logout", http.StatusInternalServerError)
	}

	http.Redirect(w, r, "/", http.StatusFound)
	return nil
}

type GitHubUser struct {
	ID    int64  `json:"id"`
	Login string `json:"login"`
	Name  string `json:"name"`
	Email string `json:"email"`
}

// handleLoginGitHub sends an oauth request to login with GitHub (Caserta
// 2015).
//
// http://pierrecaserta.com/go-oauth-facebook-github-twitter-google-plus/
func (s *server) handleLoginGitHub(w http.ResponseWriter, r *http.Request) *Error {
	// Create oauth state token.
	key := securecookie.GenerateRandomKey(oauthStateTokenSize)
	if key == nil {
		return E(nil, "error generating random oauth state token", http.StatusInternalServerError)
	}
	state := base64.URLEncoding.EncodeToString(key)

	session, err := s.sessions.Get(r, sessionName)
	if err != nil {
		return E(err, "error getting session", http.StatusInternalServerError)
	}
	session.Values["state"] = state
	session.Values["created"] = time.Now().UTC().Unix()
	if err := session.Save(r, w); err != nil {
		return E(err, "error saving session", http.StatusInternalServerError)
	}

	url := s.github.AuthCodeURL(state, oauth2.AccessTypeOnline)
	http.Redirect(w, r, url, http.StatusTemporaryRedirect)
	return nil
}

// handleLoginGitHubCallback receives the callback request returned by GitHub
// after the user has given consent to access their information (Caserta 2015):
//
// http://pierrecaserta.com/go-oauth-facebook-github-twitter-google-plus/
func (s *server) handleLoginGitHubCallback(w http.ResponseWriter, r *http.Request) *Error {
	session, err := s.sessions.Get(r, sessionName)
	if err != nil {
		return E(err, "error getting session", http.StatusInternalServerError)
	}
	val, ok := session.Values["state"]
	if !ok {
		return E(nil, "no state in session", http.StatusForbidden)
	}
	oauthState, ok := val.(string)
	if !ok {
		return E(nil, "unexpected state type", http.StatusForbidden)
	}

	// Check that the state token returned from GitHub is the same as the one
	// we generated.
	state := r.FormValue("state")
	// Constant time compare to mitigate timing attacks.
	if subtle.ConstantTimeCompare([]byte(state), []byte(oauthState)) != 1 {
		// TODO(psimika): Redirect instead of returning an error?
		e := fmt.Errorf("invalid oauth state, expected %q, got %q\n", oauthState, state)
		return E(e, "invalid oauth state", http.StatusForbidden)
	}

	// Exchange authorization code for a GitHub API token.
	code := r.FormValue("code")
	token, err := s.github.Exchange(context.Background(), code)
	if err != nil {
		return E(err, "error exchanging authorization code for token", http.StatusInternalServerError)
	}

	// Create an HTTP client that uses the GitHub API token.
	c := s.github.Client(context.Background(), token)

	// Use the client to get the consented user's info from the GitHub API.
	githubUser, err := getGitHubUser(c)
	if err != nil {
		return E(err, "could not get user from GitHub API", http.StatusInternalServerError)
	}

	user, err := s.store.GetUserByGithubID(githubUser.ID)
	// If the user is not found in our database we create them.
	if err != nil && err == petfind.ErrNotFound {
		user = &petfind.User{
			Name:     githubUser.Name,
			GithubID: githubUser.ID,
			Login:    githubUser.Login,
		}
		if cerr := s.store.CreateUser(user); cerr != nil {
			return E(cerr, "could not create user", http.StatusInternalServerError)
		}
	}
	if err != nil && err != petfind.ErrNotFound {
		return E(err, "could not retrieve user", http.StatusInternalServerError)
	}

	session.Values["userID"] = user.ID
	delete(session.Values, "state")
	if err := session.Save(r, w); err != nil {
		return E(err, "error saving session", http.StatusInternalServerError)
	}
	return nil
}

func getGitHubUser(client *http.Client) (*GitHubUser, error) {
	resp, err := client.Get("https://api.github.com/user")
	if err != nil {
		return nil, err
	}
	user := new(GitHubUser)
	if err := json.NewDecoder(resp.Body).Decode(user); err != nil {
		return nil, fmt.Errorf("could not decode user: %v", err)
	}
	return user, nil
}
<|endoftext|>


<commit_before>package web

import (
	"fmt"
	"net/http"
	"net/url"
	"time"

	"github.com/StackExchange/tsaf/_third_party/github.com/MiniProfiler/go/miniprofiler"
	"github.com/StackExchange/tsaf/_third_party/github.com/StackExchange/scollector/opentsdb"
	"github.com/StackExchange/tsaf/conf"
	"github.com/StackExchange/tsaf/expr"
)

func Expr(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
	e, err := expr.New(r.FormValue("q"))
	if err != nil {
		return nil, err
	}
	res, queries, err := e.Execute(opentsdb.NewCache(schedule.Conf.TsdbHost), t)
	if err != nil {
		return nil, err
	}

	ret := struct {
		Type    string
		Results []*expr.Result
		Queries map[string]opentsdb.Request
	}{
		e.Tree.Root.Return().String(),
		res,
		make(map[string]opentsdb.Request),
	}
	for _, q := range queries {
		if e, err := url.QueryUnescape(q.String()); err == nil {
			ret.Queries[e] = q
		}
	}
	return ret, nil
}

func Rule(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
	txt := fmt.Sprintf(`
	tsdbHost = %s
	alert _ {
		%s
	}`, schedule.Conf.TsdbHost, r.FormValue("rule"))
	c, err := conf.New("-", txt)
	if err != nil {
		return nil, err
	}
	a := c.Alerts["_"]
	if a == nil || a.Crit == nil {
		return nil, fmt.Errorf("missing crit expression")
	}
	now := time.Now().UTC()
	if fd := r.FormValue("date"); len(fd) > 0 {
		if ft := r.FormValue("time"); len(ft) > 0 {
			fd += " " + ft
		} else {
			fd += " " + now.Format("15:04")
		}
		if t, err := time.Parse("2006-01-02 15:04", fd); err == nil {
			now = t
		} else {
			return nil, err
		}
	}
	all, queries, err := a.Crit.ExecuteTime(opentsdb.NewCache(schedule.Conf.TsdbHost), t, now)
	if err != nil {
		return nil, err
	}
	var res []*expr.Result
	for _, r := range all {
		if a.Squelched(r.Group) {
			continue
		}
		res = append(res, r)
	}
	ret := struct {
		Results []*expr.Result
		Queries map[string]opentsdb.Request
	}{
		res,
		make(map[string]opentsdb.Request),
	}
	for _, q := range queries {
		if e, err := url.QueryUnescape(q.String()); err == nil {
			ret.Queries[e] = q
		}
	}
	return ret, nil
}
<commit_msg>delete newline<commit_after>package web

import (
	"fmt"
	"net/http"
	"net/url"
	"time"

	"github.com/StackExchange/tsaf/_third_party/github.com/MiniProfiler/go/miniprofiler"
	"github.com/StackExchange/tsaf/_third_party/github.com/StackExchange/scollector/opentsdb"
	"github.com/StackExchange/tsaf/conf"
	"github.com/StackExchange/tsaf/expr"
)

func Expr(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
	e, err := expr.New(r.FormValue("q"))
	if err != nil {
		return nil, err
	}
	res, queries, err := e.Execute(opentsdb.NewCache(schedule.Conf.TsdbHost), t)
	if err != nil {
		return nil, err
	}
	ret := struct {
		Type    string
		Results []*expr.Result
		Queries map[string]opentsdb.Request
	}{
		e.Tree.Root.Return().String(),
		res,
		make(map[string]opentsdb.Request),
	}
	for _, q := range queries {
		if e, err := url.QueryUnescape(q.String()); err == nil {
			ret.Queries[e] = q
		}
	}
	return ret, nil
}

func Rule(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
	txt := fmt.Sprintf(`
	tsdbHost = %s
	alert _ {
		%s
	}`, schedule.Conf.TsdbHost, r.FormValue("rule"))
	c, err := conf.New("-", txt)
	if err != nil {
		return nil, err
	}
	a := c.Alerts["_"]
	if a == nil || a.Crit == nil {
		return nil, fmt.Errorf("missing crit expression")
	}
	now := time.Now().UTC()
	if fd := r.FormValue("date"); len(fd) > 0 {
		if ft := r.FormValue("time"); len(ft) > 0 {
			fd += " " + ft
		} else {
			fd += " " + now.Format("15:04")
		}
		if t, err := time.Parse("2006-01-02 15:04", fd); err == nil {
			now = t
		} else {
			return nil, err
		}
	}
	all, queries, err := a.Crit.ExecuteTime(opentsdb.NewCache(schedule.Conf.TsdbHost), t, now)
	if err != nil {
		return nil, err
	}
	var res []*expr.Result
	for _, r := range all {
		if a.Squelched(r.Group) {
			continue
		}
		res = append(res, r)
	}
	ret := struct {
		Results []*expr.Result
		Queries map[string]opentsdb.Request
	}{
		res,
		make(map[string]opentsdb.Request),
	}
	for _, q := range queries {
		if e, err := url.QueryUnescape(q.String()); err == nil {
			ret.Queries[e] = q
		}
	}
	return ret, nil
}
<|endoftext|>


<commit_before>package weixinmp

import (
	"bytes"
	"encoding/json"
	"encoding/xml"
	"errors"
	"io/ioutil"
	"net/http"
	"reflect"
	"time"
)

const (
	// request types
	MsgTypeText      = "text"
	MsgTypeImage     = "image"
	MsgTypeVoice     = "voice"
	MsgTypeVideo     = "video"
	MsgTypeLocation  = "location"
	MsgTypeLink      = "link"
	MsgTypeEvent     = "event"
	EventSubscribe   = "subscribe"
	EventUnsubscribe = "unsubscribe"
	EventScan        = "SCAN"
	EventLocation    = "LOCATION"
	EventClick       = "CLICK"
	EventView        = "VIEW"
	// environment variables
	retryNum        = 3
	plainPreUrl     = "https://api.weixin.qq.com/cgi-bin/"
	mediaPreUrl     = "http://file.api.weixin.qq.com/cgi-bin/media/"
	accessTokenTemp = "accesstoken.temp"
)

type qrScene struct {
	ExpireSeconds int64  `json:"expire_seconds,omitempty"`
	ActionName    string `json:"action_name"`
	ActionInfo    struct {
		Scene struct {
			SceneId int64 `json:"scene_id"`
		} `json:"scene"`
	} `json:"action_info"`
}

// message structs
type textMsg struct {
	XMLName      xml.Name `xml:"xml" json:"-"`
	ToUserName   string   `json:"touser"`
	FromUserName string   `json:"-"`
	CreateTime   int64    `json:"-"`
	MsgType      string   `json:"msgtype"`
	Content      string   `json:"-"`
	Text         struct {
		Content string `xml:"-" json:"content"`
	} `xml:"-" json:"text"`
}

type imageMsg struct {
	XMLName      xml.Name `xml:"xml" json:"-"`
	ToUserName   string   `json:"touser"`
	FromUserName string   `json:"-"`
	CreateTime   int64    `json:"-"`
	MsgType      string   `json:"msgtype"`
	Image        struct {
		MediaId string `json:"media_id"`
	} `json:"image"`
}

type voiceMsg struct {
	XMLName      xml.Name `xml:"xml" json:"-"`
	ToUserName   string   `json:"touser"`
	FromUserName string   `json:"-"`
	CreateTime   int64    `json:"-"`
	MsgType      string   `json:"msgtype"`
	Voice        struct {
		MediaId string `json:"media_id"`
	} `json:"voice"`
}

type videoMsg struct {
	XMLName      xml.Name `xml:"xml" json:"-"`
	ToUserName   string   `json:"touser"`
	FromUserName string   `json:"-"`
	CreateTime   int64    `json:"-"`
	MsgType      string   `json:"msgtype"`
	Video        *Video   `json:"video"`
}

type musicMsg struct {
	XMLName      xml.Name `xml:"xml" json:"-"`
	ToUserName   string   `json:"touser"`
	FromUserName string   `json:"-"`
	CreateTime   int64    `json:"-"`
	MsgType      string   `json:"msgtype"`
	Music        *Music   `json:"music"`
}

type newsMsg struct {
	XMLName      xml.Name `xml:"xml" json:"-"`
	ToUserName   string   `json:"touser"`
	FromUserName string   `json:"-"`
	CreateTime   int64    `json:"-"`
	MsgType      string   `json:"msgtype"`
	ArticleCount int      `json:"-"`
	Articles     struct {
		Item *[]Article `xml:"item" json:"articles"`
	} `json:"news"`
}

type Video struct {
	MediaId     string `json:"media_id"`
	Title       string `json:"title"`
	Description string `json:"description"`
}

type Music struct {
	Title        string `json:"title"`
	Description  string `json:"description"`
	MediaId      string `json:"media_id"`
	MusicUrl     string `json:"musicurl"`
	HQMusicUrl   string `json:"hqmusicurl"`
	ThumbMediaId string `json:"thumb_media_id"`
}

type Article struct {
	Title       string `json:"title"`
	Description string `json:"description"`
	PicUrl      string `json:"picurl"`
	Url         string `json:"url"`
}

// weixinmp goes here
type Weixinmp struct {
	accessToken *accessToken
	Request     *Request
}

func New(token, appid, secret string) *Weixinmp {
	inst := &Weixinmp{
		accessToken: &accessToken{appid: appid, secret: secret},
		Request:     &Request{token: token},
	}
	return inst
}

// reply message methods
func (this *Weixinmp) ReplyTextMsg(rw http.ResponseWriter, content string) error {
	var msg textMsg
	msg.MsgType = "text"
	msg.Content = content
	return this.replyMsg(rw, &msg)
}

func (this *Weixinmp) ReplyImageMsg(rw http.ResponseWriter, mediaId string) error {
	var msg imageMsg
	msg.MsgType = "image"
	msg.Image.MediaId = mediaId
	return this.replyMsg(rw, &msg)
}

func (this *Weixinmp) ReplyVoiceMsg(rw http.ResponseWriter, mediaId string) error {
	var msg voiceMsg
	msg.MsgType = "voice"
	msg.Voice.MediaId = mediaId
	return this.replyMsg(rw, &msg)
}

func (this *Weixinmp) ReplyVideoMsg(rw http.ResponseWriter, video *Video) error {
	var msg videoMsg
	msg.MsgType = "video"
	msg.Video = video
	return this.replyMsg(rw, &msg)
}

func (this *Weixinmp) ReplyMusicMsg(rw http.ResponseWriter, music *Music) error {
	var msg musicMsg
	msg.MsgType = "music"
	msg.Music = music
	return this.replyMsg(rw, &msg)
}

func (this *Weixinmp) ReplyNewsMsg(rw http.ResponseWriter, articles *[]Article) error {
	var msg newsMsg
	msg.MsgType = "news"
	msg.ArticleCount = len(*articles)
	msg.Articles.Item = articles
	return this.replyMsg(rw, &msg)
}

func (this *Weixinmp) replyMsg(rw http.ResponseWriter, msg interface{}) error {
	v := reflect.ValueOf(msg).Elem()
	v.FieldByName("ToUserName").SetString(this.Request.FromUserName)
	v.FieldByName("FromUserName").SetString(this.Request.ToUserName)
	v.FieldByName("CreateTime").SetInt(time.Now().Unix())
	data, err := xml.Marshal(msg)
	if err != nil {
		return err
	}
	if _, err := rw.Write(data); err != nil {
		return err
	}
	return nil
}

// send message methods
func (this *Weixinmp) SendTextMsg(touser string, content string) error {
	var msg textMsg
	msg.MsgType = "text"
	msg.Text.Content = content
	return this.sendMsg(touser, &msg)
}

func (this *Weixinmp) SendImageMsg(touser string, mediaId string) error {
	var msg imageMsg
	msg.MsgType = "image"
	msg.Image.MediaId = mediaId
	return this.sendMsg(touser, &msg)
}

func (this *Weixinmp) SendVoiceMsg(touser string, mediaId string) error {
	var msg voiceMsg
	msg.MsgType = "voice"
	msg.Voice.MediaId = mediaId
	return this.sendMsg(touser, &msg)
}

func (this *Weixinmp) SendVideoMsg(touser string, video *Video) error {
	var msg videoMsg
	msg.MsgType = "video"
	msg.Video = video
	return this.sendMsg(touser, &msg)
}

func (this *Weixinmp) SendMusicMsg(touser string, music *Music) error {
	var msg musicMsg
	msg.MsgType = "music"
	msg.Music = music
	return this.sendMsg(touser, &msg)
}

func (this *Weixinmp) SendNewsMsg(touser string, articles *[]Article) error {
	var msg newsMsg
	msg.MsgType = "news"
	msg.Articles.Item = articles
	return this.sendMsg(touser, &msg)
}

func (this *Weixinmp) sendMsg(touser string, msg interface{}) error {
	v := reflect.ValueOf(msg).Elem()
	v.FieldByName("ToUserName").SetString(touser)
	data, err := json.Marshal(msg)
	if err != nil {
		return err
	}
	if _, err := this.post("message/custom/send", data); err != nil {
		return err
	}
	return nil
}

// get qrcode url
func (this *Weixinmp) GetQRCodeURL(ticket string) string {
	return "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=" + ticket
}

// create permanent qrcode
func (this *Weixinmp) CreateQRScene(sceneId int64) (string, error) {
	var inf qrScene
	inf.ActionName = "QR_SCENE"
	inf.ActionInfo.Scene.SceneId = sceneId
	return this.createQRCode(&inf)
}

// create temporary qrcode
func (this *Weixinmp) CreateQRLimitScene(expireSeconds, sceneId int64) (string, error) {
	var inf qrScene
	inf.ExpireSeconds = expireSeconds
	inf.ActionName = "QR_LIMIT_SCENE"
	inf.ActionInfo.Scene.SceneId = sceneId
	return this.createQRCode(&inf)
}

func (this *Weixinmp) createQRCode(inf *qrScene) (string, error) {
	data, err := json.Marshal(inf)
	if err != nil {
		return "", err
	}
	raw, err := this.post("qrcode/create", data)
	if err != nil {
		return "", err
	}
	var rtn struct {
		Ticket        string `json:"ticket"`
		ExpireSeconds int64  `json:"expire_seconds"`
	}
	if err := json.Unmarshal(raw, &rtn); err != nil {
		return "", err
	}
	return rtn.Ticket, nil
}

// send post request
func (this *Weixinmp) post(action string, data []byte) ([]byte, error) {
	// retry
	for i := 0; i < retryNum; i++ {
		token, err := this.accessToken.extract()
		if err != nil {
			if i < retryNum {
				continue
			}
			return nil, err
		}
		url := plainPreUrl + action + "?access_token=" + token
		resp, err := http.Post(url, "application/json; charset=utf-8", bytes.NewReader(data))
		defer resp.Body.Close()
		if err != nil {
			if i < retryNum {
				continue
			}
			return nil, err
		}
		raw, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			if i < retryNum {
				continue
			}
			return nil, err
		}
		var rtn struct {
			ErrCode int64  `json:"errcode"`
			ErrMsg  string `json:"errmsg"`
		}
		if err := json.Unmarshal(raw, &rtn); err != nil {
			if i < retryNum {
				continue
			}
			return nil, err
		}
		// failed
		if rtn.ErrCode != 0 {
			if i < retryNum {
				continue
			}
			return nil, errors.New(rtn.ErrMsg)
		}
		return raw, nil
	}
	return nil, errors.New("send post request failed: " + action)
}
<commit_msg>add download media file method<commit_after>package weixinmp

import (
	"bytes"
	"encoding/json"
	"encoding/xml"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"reflect"
	"time"
)

const (
	// request types
	MsgTypeText      = "text"
	MsgTypeImage     = "image"
	MsgTypeVoice     = "voice"
	MsgTypeVideo     = "video"
	MsgTypeLocation  = "location"
	MsgTypeLink      = "link"
	MsgTypeEvent     = "event"
	EventSubscribe   = "subscribe"
	EventUnsubscribe = "unsubscribe"
	EventScan        = "SCAN"
	EventLocation    = "LOCATION"
	EventClick       = "CLICK"
	EventView        = "VIEW"
	// media file types
	MediaTypeImage = "image"
	MediaTypeVoice = "voice"
	MediaTypeVideo = "video"
	MediaTypeThumb = "thumb"
	// environment variables
	retryNum        = 3
	plainPreUrl     = "https://api.weixin.qq.com/cgi-bin/"
	mediaPreUrl     = "http://file.api.weixin.qq.com/cgi-bin/media/"
	accessTokenTemp = "accesstoken.temp"
)

type qrScene struct {
	ExpireSeconds int64  `json:"expire_seconds,omitempty"`
	ActionName    string `json:"action_name"`
	ActionInfo    struct {
		Scene struct {
			SceneId int64 `json:"scene_id"`
		} `json:"scene"`
	} `json:"action_info"`
}

// message structs
type textMsg struct {
	XMLName      xml.Name `xml:"xml" json:"-"`
	ToUserName   string   `json:"touser"`
	FromUserName string   `json:"-"`
	CreateTime   int64    `json:"-"`
	MsgType      string   `json:"msgtype"`
	Content      string   `json:"-"`
	Text         struct {
		Content string `xml:"-" json:"content"`
	} `xml:"-" json:"text"`
}

type imageMsg struct {
	XMLName      xml.Name `xml:"xml" json:"-"`
	ToUserName   string   `json:"touser"`
	FromUserName string   `json:"-"`
	CreateTime   int64    `json:"-"`
	MsgType      string   `json:"msgtype"`
	Image        struct {
		MediaId string `json:"media_id"`
	} `json:"image"`
}

type voiceMsg struct {
	XMLName      xml.Name `xml:"xml" json:"-"`
	ToUserName   string   `json:"touser"`
	FromUserName string   `json:"-"`
	CreateTime   int64    `json:"-"`
	MsgType      string   `json:"msgtype"`
	Voice        struct {
		MediaId string `json:"media_id"`
	} `json:"voice"`
}

type videoMsg struct {
	XMLName      xml.Name `xml:"xml" json:"-"`
	ToUserName   string   `json:"touser"`
	FromUserName string   `json:"-"`
	CreateTime   int64    `json:"-"`
	MsgType      string   `json:"msgtype"`
	Video        *Video   `json:"video"`
}

type musicMsg struct {
	XMLName      xml.Name `xml:"xml" json:"-"`
	ToUserName   string   `json:"touser"`
	FromUserName string   `json:"-"`
	CreateTime   int64    `json:"-"`
	MsgType      string   `json:"msgtype"`
	Music        *Music   `json:"music"`
}

type newsMsg struct {
	XMLName      xml.Name `xml:"xml" json:"-"`
	ToUserName   string   `json:"touser"`
	FromUserName string   `json:"-"`
	CreateTime   int64    `json:"-"`
	MsgType      string   `json:"msgtype"`
	ArticleCount int      `json:"-"`
	Articles     struct {
		Item *[]Article `xml:"item" json:"articles"`
	} `json:"news"`
}

type Video struct {
	MediaId     string `json:"media_id"`
	Title       string `json:"title"`
	Description string `json:"description"`
}

type Music struct {
	Title        string `json:"title"`
	Description  string `json:"description"`
	MediaId      string `json:"media_id"`
	MusicUrl     string `json:"musicurl"`
	HQMusicUrl   string `json:"hqmusicurl"`
	ThumbMediaId string `json:"thumb_media_id"`
}

type Article struct {
	Title       string `json:"title"`
	Description string `json:"description"`
	PicUrl      string `json:"picurl"`
	Url         string `json:"url"`
}

// weixinmp goes here
type Weixinmp struct {
	accessToken *accessToken
	Request     *Request
}

func New(token, appid, secret string) *Weixinmp {
	inst := &Weixinmp{
		accessToken: &accessToken{appid: appid, secret: secret},
		Request:     &Request{token: token},
	}
	return inst
}

// reply message methods
func (this *Weixinmp) ReplyTextMsg(rw http.ResponseWriter, content string) error {
	var msg textMsg
	msg.MsgType = "text"
	msg.Content = content
	return this.replyMsg(rw, &msg)
}

func (this *Weixinmp) ReplyImageMsg(rw http.ResponseWriter, mediaId string) error {
	var msg imageMsg
	msg.MsgType = "image"
	msg.Image.MediaId = mediaId
	return this.replyMsg(rw, &msg)
}

func (this *Weixinmp) ReplyVoiceMsg(rw http.ResponseWriter, mediaId string) error {
	var msg voiceMsg
	msg.MsgType = "voice"
	msg.Voice.MediaId = mediaId
	return this.replyMsg(rw, &msg)
}

func (this *Weixinmp) ReplyVideoMsg(rw http.ResponseWriter, video *Video) error {
	var msg videoMsg
	msg.MsgType = "video"
	msg.Video = video
	return this.replyMsg(rw, &msg)
}

func (this *Weixinmp) ReplyMusicMsg(rw http.ResponseWriter, music *Music) error {
	var msg musicMsg
	msg.MsgType = "music"
	msg.Music = music
	return this.replyMsg(rw, &msg)
}

func (this *Weixinmp) ReplyNewsMsg(rw http.ResponseWriter, articles *[]Article) error {
	var msg newsMsg
	msg.MsgType = "news"
	msg.ArticleCount = len(*articles)
	msg.Articles.Item = articles
	return this.replyMsg(rw, &msg)
}

func (this *Weixinmp) replyMsg(rw http.ResponseWriter, msg interface{}) error {
	v := reflect.ValueOf(msg).Elem()
	v.FieldByName("ToUserName").SetString(this.Request.FromUserName)
	v.FieldByName("FromUserName").SetString(this.Request.ToUserName)
	v.FieldByName("CreateTime").SetInt(time.Now().Unix())
	data, err := xml.Marshal(msg)
	if err != nil {
		return err
	}
	if _, err := rw.Write(data); err != nil {
		return err
	}
	return nil
}

// send message methods
func (this *Weixinmp) SendTextMsg(touser string, content string) error {
	var msg textMsg
	msg.MsgType = "text"
	msg.Text.Content = content
	return this.sendMsg(touser, &msg)
}

func (this *Weixinmp) SendImageMsg(touser string, mediaId string) error {
	var msg imageMsg
	msg.MsgType = "image"
	msg.Image.MediaId = mediaId
	return this.sendMsg(touser, &msg)
}

func (this *Weixinmp) SendVoiceMsg(touser string, mediaId string) error {
	var msg voiceMsg
	msg.MsgType = "voice"
	msg.Voice.MediaId = mediaId
	return this.sendMsg(touser, &msg)
}

func (this *Weixinmp) SendVideoMsg(touser string, video *Video) error {
	var msg videoMsg
	msg.MsgType = "video"
	msg.Video = video
	return this.sendMsg(touser, &msg)
}

func (this *Weixinmp) SendMusicMsg(touser string, music *Music) error {
	var msg musicMsg
	msg.MsgType = "music"
	msg.Music = music
	return this.sendMsg(touser, &msg)
}

func (this *Weixinmp) SendNewsMsg(touser string, articles *[]Article) error {
	var msg newsMsg
	msg.MsgType = "news"
	msg.Articles.Item = articles
	return this.sendMsg(touser, &msg)
}

func (this *Weixinmp) sendMsg(touser string, msg interface{}) error {
	v := reflect.ValueOf(msg).Elem()
	v.FieldByName("ToUserName").SetString(touser)
	data, err := json.Marshal(msg)
	if err != nil {
		return err
	}
	if _, err := this.post("message/custom/send", data); err != nil {
		return err
	}
	return nil
}

// get qrcode url
func (this *Weixinmp) GetQRCodeURL(ticket string) string {
	return "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=" + ticket
}

// create permanent qrcode
func (this *Weixinmp) CreateQRScene(sceneId int64) (string, error) {
	var inf qrScene
	inf.ActionName = "QR_SCENE"
	inf.ActionInfo.Scene.SceneId = sceneId
	return this.createQRCode(&inf)
}

// create temporary qrcode
func (this *Weixinmp) CreateQRLimitScene(expireSeconds, sceneId int64) (string, error) {
	var inf qrScene
	inf.ExpireSeconds = expireSeconds
	inf.ActionName = "QR_LIMIT_SCENE"
	inf.ActionInfo.Scene.SceneId = sceneId
	return this.createQRCode(&inf)
}

func (this *Weixinmp) createQRCode(inf *qrScene) (string, error) {
	data, err := json.Marshal(inf)
	if err != nil {
		return "", err
	}
	raw, err := this.post("qrcode/create", data)
	if err != nil {
		return "", err
	}
	var rtn struct {
		Ticket        string `json:"ticket"`
		ExpireSeconds int64  `json:"expire_seconds"`
	}
	if err := json.Unmarshal(raw, &rtn); err != nil {
		return "", err
	}
	return rtn.Ticket, nil
}

// send post request
func (this *Weixinmp) post(action string, data []byte) ([]byte, error) {
	url := plainPreUrl + action + "?access_token="
	// retry
	for i := 0; i < retryNum; i++ {
		token, err := this.accessToken.extract()
		if err != nil {
			if i < retryNum {
				continue
			}
			return nil, err
		}
		resp, err := http.Post(url+token, "application/json; charset=utf-8", bytes.NewReader(data))
		defer resp.Body.Close()
		if err != nil {
			if i < retryNum {
				continue
			}
			return nil, err
		}
		raw, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			if i < retryNum {
				continue
			}
			return nil, err
		}
		var rtn struct {
			ErrCode int64  `json:"errcode"`
			ErrMsg  string `json:"errmsg"`
		}
		if err := json.Unmarshal(raw, &rtn); err != nil {
			if i < retryNum {
				continue
			}
			return nil, err
		}
		// failed
		if rtn.ErrCode != 0 {
			if i < retryNum {
				continue
			}
			return nil, errors.New(fmt.Sprintf("%d %s", rtn.ErrCode, rtn.ErrMsg))
		}
		return raw, nil
	}
	return nil, errors.New("send post request failed: " + action)
}

func (this *Weixinmp) DownloadMediaFile(mediaId, filePath string) error {
	url := fmt.Sprintf("%sget?media_id=%s&access_token=", mediaPreUrl, mediaId)
	for i := 0; i < retryNum; i++ {
		token, err := this.accessToken.extract()
		if err != nil {
			if i < retryNum {
				continue
			}
			return err
		}
		resp, err := http.Get(url + token)
		defer resp.Body.Close()
		if err != nil {
			if i < retryNum {
				continue
			}
			return err
		}
		if resp.Header.Get("Content-Type") == "text/plain" {
			if i < retryNum {
				continue
			}
			raw, err := ioutil.ReadAll(resp.Body)
			if err != nil {
				if i < retryNum {
					continue
				}
				return err
			}
			var rtn struct {
				ErrCode int64  `json:"errcode"`
				ErrMsg  string `json:"errmsg"`
			}
			if err := json.Unmarshal(raw, &rtn); err != nil {
				return err
			}
			return errors.New(fmt.Sprintf("%d %s", rtn.ErrCode, rtn.ErrMsg))
		}
		raw, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			if i < retryNum {
				continue
			}
			return err
		}
		file, err := os.Create(filePath)
		defer file.Close()
		if err != nil {
			if i < retryNum {
				continue
			}
			return err
		}
		if _, err := file.Write(raw); err != nil {
			if i < retryNum {
				continue
			}
			return err
		}
		return nil
	}
	return errors.New("download media file failed")
}
<|endoftext|>


<commit_before><commit_msg>Auto-merged master into raft-sqlite on deployment<commit_after><|endoftext|>


<commit_before>// Copyright (c) 2017, Jonathan Chappelow
// Under the ISC license.  See LICENSE.
package main

import (
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"time"

	"github.com/decred/dcrd/chaincfg"
	"github.com/decred/dcrd/chaincfg/chainhash"
	"github.com/decred/dcrrpcclient"
)

var host = flag.String("host", "127.0.0.1:9110", "wallet RPC host:port")
var user = flag.String("user", "dcrwallet", "wallet RPC username")
var pass = flag.String("pass", "bananas", "wallet RPC password")
var cert = flag.String("cert", "dcrwallet.cert", "wallet RPC TLS certificate (when notls=false)")
var notls = flag.Bool("notls", false, "Disable use of TLS for wallet connection")

var (
	activeChainParams = &chaincfg.MainNetParams
)

func main() {
	// Parse command line flags
	flag.Parse()

	// Connect to wallet RPC server
	wcl, err := ConnectRPC(*host, *user, *pass, *cert, *notls)
	if err != nil {
		log.Fatalf("Unable to connect to RPC server: %v", err)
	}

	walletInfo, err := wcl.WalletInfo()
	if err != nil {
		log.Fatalf("WalletInfo failed: %v", err)
	}

	log.Println("Wallet connected to node? ", walletInfo.DaemonConnected)

	log.Println("Listing all transactions...")
	allTxns, err := wcl.ListTransactionsCountFrom("*", 9999999, 0)
	if err != nil {
		log.Fatalf("ListTransactions failed: %v", err)
	}
	log.Println("Number of transactions: ", len(allTxns))

	// There are repeats in the list, so gather the unique ones with a map
	knownVotes := make(map[string]bool)
	for _, tx := range allTxns {
		if *tx.TxType == "vote" {
			knownVotes[tx.TxID] = true
		}
	}
	log.Println("Number of votes: ", len(knownVotes))

	waitSeconds := make([]float64, 0, len(knownVotes))
	waitBlocks := make([]int64, 0, len(knownVotes))

	for txid := range knownVotes {
		// Get ticket address from previous outpoint of Vin[1] of SSGen
		voteHash, err := chainhash.NewHashFromStr(txid)
		if err != nil {
			log.Printf("Invalid tx hash %s: %v", txid, err)
			continue
		}
		txRaw, err := wcl.GetRawTransaction(voteHash)
		if err != nil {
			log.Printf("GetRawTransaction(vote) failed: %v", err)
			continue
		}

		// Vin[1] spends the stakesubmission of the ticket purchase
		prevout := txRaw.MsgTx().TxIn[1].PreviousOutPoint
		ticketHash := &prevout.Hash
		ticketTxOutIndex := prevout.Index

		// Get block height and time for the vote
		txRawVerbose, err := wcl.GetRawTransactionVerbose(voteHash)
		if err != nil {
			log.Fatalf("GetRawTransactionVerbose(vote) failed: %v", err)
		}
		voteHeight := txRawVerbose.BlockHeight
		voteTime := time.Unix(txRawVerbose.Blocktime, 0)

		// Get block height and time for the ticket
		prevTxRaw, err := wcl.GetRawTransactionVerbose(ticketHash)
		if err != nil {
			log.Fatalf("GetRawTransactionVerbose(ticket) failed: %v", err)
		}

		// Tickets mature 256 blocks after purchase
		ticketPurchaseHeight := prevTxRaw.BlockHeight
		//ticketTime := time.Unix(prevTxRaw.Blocktime, 0)
		ticketMaturityHeight := ticketPurchaseHeight + int64(activeChainParams.TicketMaturity)
		// Get time of block at this height
		ticketMaturityBlockHash, _ := wcl.GetBlockHash(ticketMaturityHeight)
		ticketMaturityBlock, _ := wcl.GetBlockHeaderVerbose(ticketMaturityBlockHash)
		ticketMaturityTime := time.Unix(ticketMaturityBlock.Time, 0)

		// Compute time from maturity to vote
		voteWaitBlocks := voteHeight - ticketMaturityHeight
		voteWaitSeconds := voteTime.Sub(ticketMaturityTime)
		voteWaitDays := voteWaitSeconds.Hours() / 24.0

		ticketPrice := prevTxRaw.Vout[ticketTxOutIndex].Value
		log.Printf("Ticket %s... (%f DCR) mined in block %d, voted %d blocks (%.2f days) after maturity.",
			prevTxRaw.Txid[:8], ticketPrice, ticketPurchaseHeight, voteWaitBlocks, voteWaitDays)

		waitBlocks = append(waitBlocks, voteWaitBlocks)
		waitSeconds = append(waitSeconds, voteWaitSeconds.Seconds())
	}

	// Compute mean wait time in blocks and seconds
	var avgBlockWait, avgSecondWait float64
	for iv := range waitBlocks {
		avgBlockWait += float64(waitBlocks[iv])
		avgSecondWait += waitSeconds[iv]
	}
	avgBlockWait /= float64(len(waitBlocks))
	avgSecondWait /= float64(len(waitBlocks))
	log.Printf("Mean wait for %d votes: %.1f blocks, %.2f days.", len(knownVotes),
		avgBlockWait, avgSecondWait/86400.0)
}

// ConnectRPC attempts to create a new websocket connection to a legacy RPC
// server with the given credentials.
func ConnectRPC(host, user, pass, cert string, disableTLS bool) (*dcrrpcclient.Client, error) {
	var rpcCerts []byte
	var err error
	if !disableTLS {
		rpcCerts, err = ioutil.ReadFile(cert)
		if err != nil {
			return nil, fmt.Errorf("Failed to read RPC cert file at %s: %s\n",
				cert, err.Error())
		}
		log.Printf("Attempting to connect to RPC server %s as user %s "+
			"using certificate located in %s",
			host, user, cert)
	} else {
		log.Printf("Attempting to connect to RPC server %s as user %s (no TLS)",
			host, user)
	}

	connCfgDaemon := &dcrrpcclient.ConnConfig{
		Host:         host,
		Endpoint:     "ws", // websocket
		User:         user,
		Pass:         pass,
		Certificates: rpcCerts,
		DisableTLS:   disableTLS,
	}

	rpcClient, err := dcrrpcclient.New(connCfgDaemon, nil)
	if err != nil {
		return nil, fmt.Errorf("Failed to start dcrwallet RPC client: %s", err.Error())
	}

	return rpcClient, nil
}
<commit_msg>change language around listtransactions<commit_after>// Copyright (c) 2017, Jonathan Chappelow
// Under the ISC license.  See LICENSE.
package main

import (
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"time"

	"github.com/decred/dcrd/chaincfg"
	"github.com/decred/dcrd/chaincfg/chainhash"
	"github.com/decred/dcrrpcclient"
)

var host = flag.String("host", "127.0.0.1:9110", "wallet RPC host:port")
var user = flag.String("user", "dcrwallet", "wallet RPC username")
var pass = flag.String("pass", "bananas", "wallet RPC password")
var cert = flag.String("cert", "dcrwallet.cert", "wallet RPC TLS certificate (when notls=false)")
var notls = flag.Bool("notls", false, "Disable use of TLS for wallet connection")

var (
	activeChainParams = &chaincfg.MainNetParams
)

func main() {
	// Parse command line flags
	flag.Parse()

	// Connect to wallet RPC server
	wcl, err := ConnectRPC(*host, *user, *pass, *cert, *notls)
	if err != nil {
		log.Fatalf("Unable to connect to RPC server: %v", err)
	}

	walletInfo, err := wcl.WalletInfo()
	if err != nil {
		log.Fatalf("WalletInfo failed: %v", err)
	}

	log.Println("Wallet connected to node? ", walletInfo.DaemonConnected)

	log.Println("Listing all transaction inputs and outputs...")
	allTxnIOs, err := wcl.ListTransactionsCountFrom("*", 9999999, 0)
	if err != nil {
		log.Fatalf("ListTransactions failed: %v", err)
	}

	// Transactions may occur multiple times in the list, so gather the unique
	// transaction hashes with a map. We could just take the result that is
	// vout[0] of an stakegen, but this easy too.
	knownVotes := make(map[string]bool)
	for _, tx := range allTxnIOs {
		if *tx.TxType == "vote" {
			knownVotes[tx.TxID] = true
		}
	}
	log.Println("Number of votes: ", len(knownVotes))

	waitSeconds := make([]float64, 0, len(knownVotes))
	waitBlocks := make([]int64, 0, len(knownVotes))

	for txid := range knownVotes {
		// Get ticket address from previous outpoint of Vin[1] of SSGen
		voteHash, err := chainhash.NewHashFromStr(txid)
		if err != nil {
			log.Printf("Invalid tx hash %s: %v", txid, err)
			continue
		}
		txRaw, err := wcl.GetRawTransaction(voteHash)
		if err != nil {
			log.Printf("GetRawTransaction(vote) failed: %v", err)
			continue
		}

		// Vin[1] spends the stakesubmission of the ticket purchase
		prevout := txRaw.MsgTx().TxIn[1].PreviousOutPoint
		ticketHash := &prevout.Hash
		ticketTxOutIndex := prevout.Index

		// Get block height and time for the vote
		txRawVerbose, err := wcl.GetRawTransactionVerbose(voteHash)
		if err != nil {
			log.Fatalf("GetRawTransactionVerbose(vote) failed: %v", err)
		}
		voteHeight := txRawVerbose.BlockHeight
		voteTime := time.Unix(txRawVerbose.Blocktime, 0)

		// Get block height and time for the ticket
		prevTxRaw, err := wcl.GetRawTransactionVerbose(ticketHash)
		if err != nil {
			log.Fatalf("GetRawTransactionVerbose(ticket) failed: %v", err)
		}

		// Tickets mature 256 blocks after purchase
		ticketPurchaseHeight := prevTxRaw.BlockHeight
		//ticketTime := time.Unix(prevTxRaw.Blocktime, 0)
		ticketMaturityHeight := ticketPurchaseHeight + int64(activeChainParams.TicketMaturity)
		// Get time of block at this height
		ticketMaturityBlockHash, _ := wcl.GetBlockHash(ticketMaturityHeight)
		ticketMaturityBlock, _ := wcl.GetBlockHeaderVerbose(ticketMaturityBlockHash)
		ticketMaturityTime := time.Unix(ticketMaturityBlock.Time, 0)

		// Compute time from maturity to vote
		voteWaitBlocks := voteHeight - ticketMaturityHeight
		voteWaitSeconds := voteTime.Sub(ticketMaturityTime)
		voteWaitDays := voteWaitSeconds.Hours() / 24.0

		ticketPrice := prevTxRaw.Vout[ticketTxOutIndex].Value
		log.Printf("Ticket %s... (%f DCR) mined in block %d, voted %d blocks (%.2f days) after maturity.",
			prevTxRaw.Txid[:8], ticketPrice, ticketPurchaseHeight, voteWaitBlocks, voteWaitDays)

		waitBlocks = append(waitBlocks, voteWaitBlocks)
		waitSeconds = append(waitSeconds, voteWaitSeconds.Seconds())
	}

	// Compute mean wait time in blocks and seconds
	var avgBlockWait, avgSecondWait float64
	for iv := range waitBlocks {
		avgBlockWait += float64(waitBlocks[iv])
		avgSecondWait += waitSeconds[iv]
	}
	avgBlockWait /= float64(len(waitBlocks))
	avgSecondWait /= float64(len(waitBlocks))
	log.Printf("Mean wait for %d votes: %.1f blocks, %.2f days.", len(knownVotes),
		avgBlockWait, avgSecondWait/86400.0)
}

// ConnectRPC attempts to create a new websocket connection to a legacy RPC
// server with the given credentials.
func ConnectRPC(host, user, pass, cert string, disableTLS bool) (*dcrrpcclient.Client, error) {
	var rpcCerts []byte
	var err error
	if !disableTLS {
		rpcCerts, err = ioutil.ReadFile(cert)
		if err != nil {
			return nil, fmt.Errorf("Failed to read RPC cert file at %s: %s\n",
				cert, err.Error())
		}
		log.Printf("Attempting to connect to RPC server %s as user %s "+
			"using certificate located in %s",
			host, user, cert)
	} else {
		log.Printf("Attempting to connect to RPC server %s as user %s (no TLS)",
			host, user)
	}

	connCfgDaemon := &dcrrpcclient.ConnConfig{
		Host:         host,
		Endpoint:     "ws", // websocket
		User:         user,
		Pass:         pass,
		Certificates: rpcCerts,
		DisableTLS:   disableTLS,
	}

	rpcClient, err := dcrrpcclient.New(connCfgDaemon, nil)
	if err != nil {
		return nil, fmt.Errorf("Failed to start dcrwallet RPC client: %s", err.Error())
	}

	return rpcClient, nil
}
<|endoftext|>


<commit_before>package interceptor

import (
	"fmt"
	"reflect"
	"testing"

	"github.com/stretchr/testify/require"
)

type mockIPTables struct {
	t *testing.T

	// Map from "table-name chain-name" to lists of rules
	chains map[string][][]string
}

func newMockIPTables(t *testing.T) mockIPTables {
	return mockIPTables{
		t: t,
		chains: map[string][][]string{
			"nat PREROUTING": make([][]string, 0),
			"filter FORWARD": make([][]string, 0),
			"filter INPUT":   make([][]string, 0),
		},
	}
}

type mockExitError bool

func (e mockExitError) Success() bool { return bool(e) }
func (e mockExitError) Error() string { return "mockExitError" }

func (m mockIPTables) key(args []string) string {
	return fmt.Sprintf("%s %s", args[1], args[3])
}

func (m mockIPTables) error(msg ...interface{}) ([]byte, error) {
	return ([]byte)(fmt.Sprint(msg)), mockExitError(false)
}

func (m mockIPTables) invoke(args []string) ([]byte, error) {
	require.Equal(m.t, "-t", args[0])

	switch args[2] {
	case "-N":
		k := m.key(args)
		if _, present := m.chains[k]; present {
			return m.error("iptables: Chain already exists.")
		}

		if len(args) > 4 {
			return m.error("Bad argument '", args[4], "'")
		}

		m.chains[k] = make([][]string, 0)

	case "-X":
		k := m.key(args)
		if _, present := m.chains[k]; !present {
			return m.error("iptables: No chain/target/match by that name.")
		}

		if len(args) > 4 {
			return m.error("Bad argument '", args[4], "'")
		}

		delete(m.chains, k)

	case "-F":
		k := m.key(args)
		if _, present := m.chains[k]; !present {
			return m.error("iptables: No chain/target/match by that name.")
		}

		if len(args) > 4 {
			return m.error("Bad argument '", args[4], "'")
		}

		m.chains[k] = m.chains[k][:0]

	case "-I":
		k := m.key(args)
		if _, present := m.chains[k]; !present {
			return m.error("iptables: No chain/target/match by that name.")
		}

		// no rulenum support needed for now

		m.chains[k] = append([][]string{args[4:]}, m.chains[k]...)

	case "-D":
		k := m.key(args)
		if _, present := m.chains[k]; !present {
			return m.error("iptables: No chain/target/match by that name.")
		}

		for i, r := range m.chains[k] {
			if reflect.DeepEqual(args[4:], r) {
				m.chains[k] = append(m.chains[k][:i], m.chains[k][i+1:]...)
				return nil, nil
			}
		}

		return m.error("iptables: Bad rule (does a matching rule exist in that chain?).")

	default:
		m.t.Log("Unknown iptables option ", args[2])
		m.t.Fail()
		return m.error("Unknown option ", args[2])
	}

	return nil, nil
}

func TestDaemon(t *testing.T) {
	i := Start([]string{"interceptor"}, newMockIPTables(t).invoke)

	select {
	case err := <-i.Fatal:
		t.Fatal(err)
	default:
	}

	i.Stop()
}
<commit_msg>Cheeck that the balancer cleans up iptables when stopping<commit_after>package interceptor

import (
	"fmt"
	"reflect"
	"testing"

	"github.com/stretchr/testify/require"
)

type mockIPTables struct {
	t *testing.T

	// Map from "table-name chain-name" to lists of rules
	chains map[string][][]string
}

var builtinChains = []string{
	"nat PREROUTING",
	"filter FORWARD",
	"filter INPUT",
}

func newMockIPTables(t *testing.T) mockIPTables {
	m := mockIPTables{
		t:      t,
		chains: make(map[string][][]string),
	}

	for _, c := range builtinChains {
		m.chains[c] = make([][]string, 0)
	}

	return m
}

type mockExitError bool

func (e mockExitError) Success() bool { return bool(e) }
func (e mockExitError) Error() string { return "mockExitError" }

func (m mockIPTables) key(args []string) string {
	return fmt.Sprintf("%s %s", args[1], args[3])
}

func (m mockIPTables) error(msg ...interface{}) ([]byte, error) {
	return ([]byte)(fmt.Sprint(msg)), mockExitError(false)
}

func (m mockIPTables) invoke(args []string) ([]byte, error) {
	require.Equal(m.t, "-t", args[0])

	switch args[2] {
	case "-N":
		k := m.key(args)
		if _, present := m.chains[k]; present {
			return m.error("iptables: Chain already exists.")
		}

		if len(args) > 4 {
			return m.error("Bad argument '", args[4], "'")
		}

		m.chains[k] = make([][]string, 0)

	case "-X":
		k := m.key(args)
		if _, present := m.chains[k]; !present {
			return m.error("iptables: No chain/target/match by that name.")
		}

		if len(args) > 4 {
			return m.error("Bad argument '", args[4], "'")
		}

		delete(m.chains, k)

	case "-F":
		k := m.key(args)
		if _, present := m.chains[k]; !present {
			return m.error("iptables: No chain/target/match by that name.")
		}

		if len(args) > 4 {
			return m.error("Bad argument '", args[4], "'")
		}

		m.chains[k] = m.chains[k][:0]

	case "-I":
		k := m.key(args)
		if _, present := m.chains[k]; !present {
			return m.error("iptables: No chain/target/match by that name.")
		}

		// no rulenum support needed for now

		m.chains[k] = append([][]string{args[4:]}, m.chains[k]...)

	case "-D":
		k := m.key(args)
		if _, present := m.chains[k]; !present {
			return m.error("iptables: No chain/target/match by that name.")
		}

		for i, r := range m.chains[k] {
			if reflect.DeepEqual(args[4:], r) {
				m.chains[k] = append(m.chains[k][:i], m.chains[k][i+1:]...)
				return nil, nil
			}
		}

		return m.error("iptables: Bad rule (does a matching rule exist in that chain?).")

	default:
		m.t.Log("Unknown iptables option ", args[2])
		m.t.Fail()
		return m.error("Unknown option ", args[2])
	}

	return nil, nil
}

func TestDaemon(t *testing.T) {
	iptables := newMockIPTables(t)
	i := Start([]string{"interceptor"}, iptables.invoke)

	select {
	case err := <-i.Fatal:
		t.Fatal(err)
	default:
	}

	i.Stop()

	// check that iptables was cleaned up
	for c, _ := range iptables.chains {
		require.Contains(t, builtinChains, c)
	}
}
<|endoftext|>


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

package comebackfs

import (
	"fmt"
	"log"
	"os"
	"time"

	"golang.org/x/net/context"

	"github.com/jacobsa/comeback/internal/blob"
	"github.com/jacobsa/comeback/internal/fs"
	pkgfs "github.com/jacobsa/comeback/internal/fs"
	"github.com/jacobsa/fuse"
	"github.com/jacobsa/fuse/fuseops"
	"github.com/jacobsa/fuse/fuseutil"
	"github.com/jacobsa/syncutil"
)

// Create a read-only file system for browsing the backup rooted by the
// supplied score. All inodes will be owned by the supplied UID/GID pair.
func NewFileSystem(
	uid uint32,
	gid uint32,
	rootScore blob.Score,
	blobStore blob.Store) (fs fuseutil.FileSystem, err error) {
	// Create the file system.
	typed := &fileSystem{
		uid:       uid,
		gid:       gid,
		blobStore: blobStore,
		inodes:    make(map[fuseops.InodeID]*inodeRecord),
	}

	fs = typed
	typed.mu = syncutil.NewInvariantMutex(typed.checkInvariants)

	// Set up the root inode.
	typed.Lock()
	defer typed.Unlock()

	rootEntry := &pkgfs.DirectoryEntry{
		Type:        pkgfs.TypeDirectory,
		Name:        "",
		Permissions: 0500,
		Inode:       fuseops.RootInodeID,
		Scores:      []blob.Score{rootScore},
	}

	_, err = typed.lookUpOrCreateInode(rootEntry)
	if err != nil {
		err = fmt.Errorf("Creating root inode: %v", err)
		return
	}

	return
}

////////////////////////////////////////////////////////////////////////
// Internal
////////////////////////////////////////////////////////////////////////

type fileSystem struct {
	fuseutil.NotImplementedFileSystem

	uid uint32
	gid uint32

	blobStore blob.Store

	/////////////////////////
	// Mutable data
	/////////////////////////

	// LOCK ORDERING:
	//
	// Let FS be the file system. Define a strict partial ordering < by:
	//
	// *   For any inode I,  I < FS.
	//
	// and follow the rule "acquire B while holding A only if A < B".
	//
	// In other words:
	//
	// *   Don't hold more than one inode lock at a time.
	// *   Don't acquire an inode lock before the file system lock.
	//
	// The intuition is that inode locks are held for long operations, but the
	// file system lock is lightweight and must not be.
	mu syncutil.InvariantMutex

	// The inodes we currently know, along with the lookup counts. The inode IDs
	// come from the directory listings stored in GCS.
	//
	// INVARIANT: For all v, v.lookupCount > 0
	//
	// GUARDED_BY(mu)
	inodes map[fuseops.InodeID]*inodeRecord
}

// An inode and its lookup count.
type inodeRecord struct {
	lookupCount uint64
	in          inode
}

// LOCKS_REQUIRED(fs)
func (fs *fileSystem) checkInvariants() {
	// INVARIANT: For all v, v.lookupCount > 0
	for k, v := range fs.inodes {
		if !(v.lookupCount > 0) {
			log.Fatalf("Inode %d has invalid lookupCount %d", k, v.lookupCount)
		}
	}
}

// Given a directory entry within the file system, look up an inode for the
// entry if it already exists. If not, create and register one. In either case,
// increment the lookup count.
//
// LOCKS_REQUIRED(fs)
func (fs *fileSystem) lookUpOrCreateInode(e *fs.DirectoryEntry) (
	in inode,
	err error) {
	id := fuseops.InodeID(e.Inode)

	// Do we already have an inode with the given ID?
	if rec, ok := fs.inodes[id]; ok {
		in = rec.in
		rec.lookupCount++
		return
	}

	// Create and register one.
	in, err = createInode(e, fs.uid, fs.gid, fs.blobStore)
	if err != nil {
		err = fmt.Errorf("createInode: %v", err)
		return
	}

	fs.inodes[id] = &inodeRecord{
		lookupCount: 1,
		in:          in,
	}

	return
}

// Create an inode for the supplied directory entry. The UID and GID are
// ignored in favor of the the supplied values.
func createInode(
	e *fs.DirectoryEntry,
	uid uint32,
	gid uint32,
	blobStore blob.Store) (in inode, err error) {
	switch e.Type {
	case fs.TypeDirectory:
		// Check the score count.
		if len(e.Scores) != 1 {
			err = fmt.Errorf(
				"Unexpected score count for directory: %d",
				len(e.Scores))
			return
		}

		// Create the inode.
		in = newDirInode(
			fuseops.InodeAttributes{
				Nlink: 1,
				Mode:  e.Permissions | os.ModeDir,
				Mtime: e.MTime,
				Ctime: e.MTime,
				Uid:   uid,
				Gid:   gid,
			},
			e.Scores[0],
			blobStore)

		return

	default:
		err = fmt.Errorf("Don't know how to handle type %d", e.Type)
		return
	}
}

////////////////////////////////////////////////////////////////////////
// Public interface
////////////////////////////////////////////////////////////////////////

// LOCKS_EXCLUDED(fs)
func (fs *fileSystem) Lock() {
	fs.mu.Lock()
}

// LOCKS_REQUIRED(fs)
func (fs *fileSystem) Unlock() {
	fs.mu.Unlock()
}

// LOCKS_EXCLUDED(fs)
func (fs *fileSystem) GetInodeAttributes(
	ctx context.Context,
	op *fuseops.GetInodeAttributesOp) (err error) {
	// Find the inode.
	fs.Lock()
	rec := fs.inodes[op.Inode]
	fs.Unlock()

	if rec == nil {
		log.Fatalf("Inode %d not found", op.Inode)
	}

	in := rec.in
	in.Lock()
	defer in.Unlock()

	// Get its attributes. We don't care how long the kernel caches them, because
	// we are immutable.
	op.Attributes = in.Attributes()
	op.AttributesExpiration = time.Now().Add(24 * time.Hour)

	return
}

// LOCKS_EXCLUDED(fs)
func (fs *fileSystem) LookUpInode(
	ctx context.Context,
	op *fuseops.LookUpInodeOp) (err error) {
	// Find the parent.
	fs.Lock()
	parentRec, _ := fs.inodes[op.Parent]
	fs.Unlock()

	if parentRec == nil {
		log.Fatalf("Inode %d not found", op.Parent)
	}

	parent := parentRec.in.(*dirInode)

	// Find an entry for the child within it.
	parent.Lock()
	e, err := parent.LookUpChild(ctx, op.Name)
	parent.Unlock()

	if err != nil {
		err = fmt.Errorf("LookUpChild: %v", err)
		return
	}

	if e == nil {
		err = fuse.ENOENT
		return
	}

	// Find or create the inode.
	fs.Lock()
	in, err := fs.lookUpOrCreateInode(e)
	fs.Unlock()

	if err != nil {
		err = fmt.Errorf("lookUpOrCreateInode: %v", err)
		return
	}

	// Fill out the response.
	in.Lock()
	defer in.Unlock()

	op.Entry.Child = fuseops.InodeID(e.Inode)
	op.Entry.Attributes = in.Attributes()
	op.Entry.AttributesExpiration = time.Now().Add(24 * time.Hour)
	op.Entry.EntryExpiration = time.Now().Add(24 * time.Hour)

	return
}

// LOCKS_EXCLUDED(fs)
func (fs *fileSystem) ForgetInode(
	ctx context.Context,
	op *fuseops.ForgetInodeOp) (err error) {
	fs.Lock()
	defer fs.Unlock()

	// Find the inode.
	rec := fs.inodes[op.Inode]
	if rec == nil {
		log.Fatalf("Inode %d not found", op.Inode)
	}

	// Decrement its lookup count.
	if rec.lookupCount < op.N {
		log.Fatalf(
			"Inode %d has lookup count %d, decrementing by %d",
			op.Inode,
			rec.lookupCount,
			op.N)
	}

	rec.lookupCount -= op.N
	if rec.lookupCount == 0 {
		delete(fs.inodes, op.Inode)
	}

	return
}

// LOCKS_EXCLUDED(fs)
func (fs *fileSystem) OpenDir(
	ctx context.Context,
	op *fuseops.OpenDirOp) (err error) {
	// Nothing interesting to do since we don't use directory handles.
	return
}

// LOCKS_EXCLUDED(fs)
func (fs *fileSystem) ReadDir(
	ctx context.Context,
	op *fuseops.ReadDirOp) (err error) {
	// Find the inode.
	fs.Lock()
	rec, _ := fs.inodes[op.Inode]
	fs.Unlock()

	if rec == nil {
		log.Fatalf("Inode %d not found", op.Inode)
	}

	d := rec.in.(*dirInode)

	// Read.
	d.Lock()
	err = d.Read(ctx, op)
	d.Unlock()

	return
}
<commit_msg>Support creating file inodes.<commit_after>// Copyright 2015 Aaron Jacobs. All Rights Reserved.
// Author: aaronjjacobs@gmail.com (Aaron Jacobs)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package comebackfs

import (
	"fmt"
	"log"
	"os"
	"time"

	"golang.org/x/net/context"

	"github.com/jacobsa/comeback/internal/blob"
	"github.com/jacobsa/comeback/internal/fs"
	pkgfs "github.com/jacobsa/comeback/internal/fs"
	"github.com/jacobsa/fuse"
	"github.com/jacobsa/fuse/fuseops"
	"github.com/jacobsa/fuse/fuseutil"
	"github.com/jacobsa/syncutil"
)

// Create a read-only file system for browsing the backup rooted by the
// supplied score. All inodes will be owned by the supplied UID/GID pair.
func NewFileSystem(
	uid uint32,
	gid uint32,
	rootScore blob.Score,
	blobStore blob.Store) (fs fuseutil.FileSystem, err error) {
	// Create the file system.
	typed := &fileSystem{
		uid:       uid,
		gid:       gid,
		blobStore: blobStore,
		inodes:    make(map[fuseops.InodeID]*inodeRecord),
	}

	fs = typed
	typed.mu = syncutil.NewInvariantMutex(typed.checkInvariants)

	// Set up the root inode.
	typed.Lock()
	defer typed.Unlock()

	rootEntry := &pkgfs.DirectoryEntry{
		Type:        pkgfs.TypeDirectory,
		Name:        "",
		Permissions: 0500,
		Inode:       fuseops.RootInodeID,
		Scores:      []blob.Score{rootScore},
	}

	_, err = typed.lookUpOrCreateInode(rootEntry)
	if err != nil {
		err = fmt.Errorf("Creating root inode: %v", err)
		return
	}

	return
}

////////////////////////////////////////////////////////////////////////
// Internal
////////////////////////////////////////////////////////////////////////

type fileSystem struct {
	fuseutil.NotImplementedFileSystem

	uid uint32
	gid uint32

	blobStore blob.Store

	/////////////////////////
	// Mutable data
	/////////////////////////

	// LOCK ORDERING:
	//
	// Let FS be the file system. Define a strict partial ordering < by:
	//
	// *   For any inode I,  I < FS.
	//
	// and follow the rule "acquire B while holding A only if A < B".
	//
	// In other words:
	//
	// *   Don't hold more than one inode lock at a time.
	// *   Don't acquire an inode lock before the file system lock.
	//
	// The intuition is that inode locks are held for long operations, but the
	// file system lock is lightweight and must not be.
	mu syncutil.InvariantMutex

	// The inodes we currently know, along with the lookup counts. The inode IDs
	// come from the directory listings stored in GCS.
	//
	// INVARIANT: For all v, v.lookupCount > 0
	//
	// GUARDED_BY(mu)
	inodes map[fuseops.InodeID]*inodeRecord
}

// An inode and its lookup count.
type inodeRecord struct {
	lookupCount uint64
	in          inode
}

// LOCKS_REQUIRED(fs)
func (fs *fileSystem) checkInvariants() {
	// INVARIANT: For all v, v.lookupCount > 0
	for k, v := range fs.inodes {
		if !(v.lookupCount > 0) {
			log.Fatalf("Inode %d has invalid lookupCount %d", k, v.lookupCount)
		}
	}
}

// Given a directory entry within the file system, look up an inode for the
// entry if it already exists. If not, create and register one. In either case,
// increment the lookup count.
//
// LOCKS_REQUIRED(fs)
func (fs *fileSystem) lookUpOrCreateInode(e *fs.DirectoryEntry) (
	in inode,
	err error) {
	id := fuseops.InodeID(e.Inode)

	// Do we already have an inode with the given ID?
	if rec, ok := fs.inodes[id]; ok {
		in = rec.in
		rec.lookupCount++
		return
	}

	// Create and register one.
	in, err = createInode(e, fs.uid, fs.gid, fs.blobStore)
	if err != nil {
		err = fmt.Errorf("createInode: %v", err)
		return
	}

	fs.inodes[id] = &inodeRecord{
		lookupCount: 1,
		in:          in,
	}

	return
}

// Create an inode for the supplied directory entry. The UID and GID are
// ignored in favor of the the supplied values.
func createInode(
	e *fs.DirectoryEntry,
	uid uint32,
	gid uint32,
	blobStore blob.Store) (in inode, err error) {
	switch e.Type {
	case fs.TypeDirectory:
		// Check the score count.
		if len(e.Scores) != 1 {
			err = fmt.Errorf(
				"Unexpected score count for directory: %d",
				len(e.Scores))
			return
		}

		// Create the inode.
		in = newDirInode(
			fuseops.InodeAttributes{
				Nlink: 1,
				Mode:  e.Permissions | os.ModeDir,
				Mtime: e.MTime,
				Ctime: e.MTime,
				Uid:   uid,
				Gid:   gid,
			},
			e.Scores[0],
			blobStore)

		return

	case fs.TypeFile:
		in = newFileInode(
			fuseops.InodeAttributes{
				Size:  e.Size,
				Nlink: 1,
				Mode:  e.Permissions,
				Mtime: e.MTime,
				Ctime: e.MTime,
				Uid:   uid,
				Gid:   gid,
			},
			e.Scores,
			blobStore)

		return

	default:
		err = fmt.Errorf("Don't know how to handle type %d", e.Type)
		return
	}
}

////////////////////////////////////////////////////////////////////////
// Public interface
////////////////////////////////////////////////////////////////////////

// LOCKS_EXCLUDED(fs)
func (fs *fileSystem) Lock() {
	fs.mu.Lock()
}

// LOCKS_REQUIRED(fs)
func (fs *fileSystem) Unlock() {
	fs.mu.Unlock()
}

// LOCKS_EXCLUDED(fs)
func (fs *fileSystem) GetInodeAttributes(
	ctx context.Context,
	op *fuseops.GetInodeAttributesOp) (err error) {
	// Find the inode.
	fs.Lock()
	rec := fs.inodes[op.Inode]
	fs.Unlock()

	if rec == nil {
		log.Fatalf("Inode %d not found", op.Inode)
	}

	in := rec.in
	in.Lock()
	defer in.Unlock()

	// Get its attributes. We don't care how long the kernel caches them, because
	// we are immutable.
	op.Attributes = in.Attributes()
	op.AttributesExpiration = time.Now().Add(24 * time.Hour)

	return
}

// LOCKS_EXCLUDED(fs)
func (fs *fileSystem) LookUpInode(
	ctx context.Context,
	op *fuseops.LookUpInodeOp) (err error) {
	// Find the parent.
	fs.Lock()
	parentRec, _ := fs.inodes[op.Parent]
	fs.Unlock()

	if parentRec == nil {
		log.Fatalf("Inode %d not found", op.Parent)
	}

	parent := parentRec.in.(*dirInode)

	// Find an entry for the child within it.
	parent.Lock()
	e, err := parent.LookUpChild(ctx, op.Name)
	parent.Unlock()

	if err != nil {
		err = fmt.Errorf("LookUpChild: %v", err)
		return
	}

	if e == nil {
		err = fuse.ENOENT
		return
	}

	// Find or create the inode.
	fs.Lock()
	in, err := fs.lookUpOrCreateInode(e)
	fs.Unlock()

	if err != nil {
		err = fmt.Errorf("lookUpOrCreateInode: %v", err)
		return
	}

	// Fill out the response.
	in.Lock()
	defer in.Unlock()

	op.Entry.Child = fuseops.InodeID(e.Inode)
	op.Entry.Attributes = in.Attributes()
	op.Entry.AttributesExpiration = time.Now().Add(24 * time.Hour)
	op.Entry.EntryExpiration = time.Now().Add(24 * time.Hour)

	return
}

// LOCKS_EXCLUDED(fs)
func (fs *fileSystem) ForgetInode(
	ctx context.Context,
	op *fuseops.ForgetInodeOp) (err error) {
	fs.Lock()
	defer fs.Unlock()

	// Find the inode.
	rec := fs.inodes[op.Inode]
	if rec == nil {
		log.Fatalf("Inode %d not found", op.Inode)
	}

	// Decrement its lookup count.
	if rec.lookupCount < op.N {
		log.Fatalf(
			"Inode %d has lookup count %d, decrementing by %d",
			op.Inode,
			rec.lookupCount,
			op.N)
	}

	rec.lookupCount -= op.N
	if rec.lookupCount == 0 {
		delete(fs.inodes, op.Inode)
	}

	return
}

// LOCKS_EXCLUDED(fs)
func (fs *fileSystem) OpenDir(
	ctx context.Context,
	op *fuseops.OpenDirOp) (err error) {
	// Nothing interesting to do since we don't use directory handles.
	return
}

// LOCKS_EXCLUDED(fs)
func (fs *fileSystem) ReadDir(
	ctx context.Context,
	op *fuseops.ReadDirOp) (err error) {
	// Find the inode.
	fs.Lock()
	rec, _ := fs.inodes[op.Inode]
	fs.Unlock()

	if rec == nil {
		log.Fatalf("Inode %d not found", op.Inode)
	}

	d := rec.in.(*dirInode)

	// Read.
	d.Lock()
	err = d.Read(ctx, op)
	d.Unlock()

	return
}
<|endoftext|>