refactor(api): API overhaul (#392)

pull/402/head
Anthony Lapenna 2016-12-18 18:21:29 +13:00 committed by GitHub
parent d9f6124609
commit 0a38bba874
36 changed files with 1275 additions and 869 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@ node_modules
bower_components
dist
portainer-checksum.txt
api/cmd/portainer/portainer-*

View File

@ -1,102 +0,0 @@
package main
import (
"crypto/tls"
"errors"
"github.com/gorilla/securecookie"
"log"
"net/http"
"net/url"
)
type (
api struct {
endpoint *url.URL
bindAddress string
assetPath string
dataPath string
tlsConfig *tls.Config
templatesURL string
dataStore *dataStore
secret []byte
}
apiConfig struct {
Endpoint string
BindAddress string
AssetPath string
DataPath string
SwarmSupport bool
TLSEnabled bool
TLSCACertPath string
TLSCertPath string
TLSKeyPath string
TemplatesURL string
}
)
const (
datastoreFileName = "portainer.db"
)
var (
errSecretKeyGeneration = errors.New("Unable to generate secret key to sign JWT")
)
func (a *api) run(settings *Settings) {
err := a.initDatabase()
if err != nil {
log.Fatal(err)
}
defer a.cleanUp()
handler := a.newHandler(settings)
log.Printf("Starting portainer on %s", a.bindAddress)
if err := http.ListenAndServe(a.bindAddress, handler); err != nil {
log.Fatal(err)
}
}
func (a *api) cleanUp() {
a.dataStore.cleanUp()
}
func (a *api) initDatabase() error {
dataStore, err := newDataStore(a.dataPath + "/" + datastoreFileName)
if err != nil {
return err
}
err = dataStore.initDataStore()
if err != nil {
return err
}
a.dataStore = dataStore
return nil
}
func newAPI(apiConfig apiConfig) *api {
endpointURL, err := url.Parse(apiConfig.Endpoint)
if err != nil {
log.Fatal(err)
}
secret := securecookie.GenerateRandomKey(32)
if secret == nil {
log.Fatal(errSecretKeyGeneration)
}
var tlsConfig *tls.Config
if apiConfig.TLSEnabled {
tlsConfig = newTLSConfig(apiConfig.TLSCACertPath, apiConfig.TLSCertPath, apiConfig.TLSKeyPath)
}
return &api{
endpoint: endpointURL,
bindAddress: apiConfig.BindAddress,
assetPath: apiConfig.AssetPath,
dataPath: apiConfig.DataPath,
tlsConfig: tlsConfig,
templatesURL: apiConfig.TemplatesURL,
secret: secret,
}
}

View File

@ -1,88 +0,0 @@
package main
import (
"encoding/json"
"github.com/asaskevich/govalidator"
"golang.org/x/crypto/bcrypt"
"io/ioutil"
"log"
"net/http"
)
type (
credentials struct {
Username string `valid:"alphanum,required"`
Password string `valid:"length(8)"`
}
authResponse struct {
JWT string `json:"jwt"`
}
)
func hashPassword(password string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", nil
}
return string(hash), nil
}
func checkPasswordValidity(password string, hash string) error {
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
}
// authHandler defines a handler function used to authenticate users
func (api *api) authHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
w.Header().Set("Allow", "POST")
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Unable to parse request body", http.StatusBadRequest)
return
}
var credentials credentials
err = json.Unmarshal(body, &credentials)
if err != nil {
http.Error(w, "Unable to parse credentials", http.StatusBadRequest)
return
}
_, err = govalidator.ValidateStruct(credentials)
if err != nil {
http.Error(w, "Invalid credentials format", http.StatusBadRequest)
return
}
var username = credentials.Username
var password = credentials.Password
u, err := api.dataStore.getUserByUsername(username)
if err != nil {
log.Printf("User not found: %s", username)
http.Error(w, "User not found", http.StatusNotFound)
return
}
err = checkPasswordValidity(password, u.Password)
if err != nil {
log.Printf("Invalid credentials for user: %s", username)
http.Error(w, "Invalid credentials", http.StatusUnprocessableEntity)
return
}
token, err := api.generateJWTToken(username)
if err != nil {
log.Printf("Unable to generate JWT token: %s", err.Error())
http.Error(w, "Unable to generate JWT token", http.StatusInternalServerError)
return
}
response := authResponse{
JWT: token,
}
json.NewEncoder(w).Encode(response)
}

58
api/bolt/datastore.go Normal file
View File

@ -0,0 +1,58 @@
package bolt
import (
"github.com/boltdb/bolt"
"time"
)
// Store defines the implementation of portainer.DataStore using
// BoltDB as the storage system.
type Store struct {
// Path where is stored the BoltDB database.
Path string
// Services
UserService *UserService
db *bolt.DB
}
const (
databaseFileName = "portainer.db"
userBucketName = "users"
)
// NewStore initializes a new Store and the associated services
func NewStore(storePath string) *Store {
store := &Store{
Path: storePath,
UserService: &UserService{},
}
store.UserService.store = store
return store
}
// Open opens and initializes the BoltDB database.
func (store *Store) Open() error {
path := store.Path + "/" + databaseFileName
db, err := bolt.Open(path, 0600, &bolt.Options{Timeout: 1 * time.Second})
if err != nil {
return err
}
store.db = db
return db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(userBucketName))
if err != nil {
return err
}
return nil
})
}
// Close closes the BoltDB database.
func (store *Store) Close() error {
if store.db != nil {
return store.db.Close()
}
return nil
}

View File

@ -0,0 +1,17 @@
package internal
import (
"github.com/portainer/portainer"
"encoding/json"
)
// MarshalUser encodes a user to binary format.
func MarshalUser(user *portainer.User) ([]byte, error) {
return json.Marshal(user)
}
// UnmarshalUser decodes a user from a binary data.
func UnmarshalUser(data []byte, user *portainer.User) error {
return json.Unmarshal(data, user)
}

56
api/bolt/user_service.go Normal file
View File

@ -0,0 +1,56 @@
package bolt
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
// UserService represents a service for managing users.
type UserService struct {
store *Store
}
// User returns a user by username.
func (service *UserService) User(username string) (*portainer.User, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
value := bucket.Get([]byte(username))
if value == nil {
return portainer.ErrUserNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return nil, err
}
var user portainer.User
err = internal.UnmarshalUser(data, &user)
if err != nil {
return nil, err
}
return &user, nil
}
// UpdateUser saves a user.
func (service *UserService) UpdateUser(user *portainer.User) error {
data, err := internal.MarshalUser(user)
if err != nil {
return err
}
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
err = bucket.Put([]byte(user.Username), data)
if err != nil {
return err
}
return nil
})
}

59
api/cli/cli.go Normal file
View File

@ -0,0 +1,59 @@
package cli
import (
"github.com/portainer/portainer"
"gopkg.in/alecthomas/kingpin.v2"
"os"
"strings"
)
// Service implements the CLIService interface
type Service struct{}
const (
errInvalidEnpointProtocol = portainer.Error("Invalid endpoint protocol: Portainer only supports unix:// or tcp://")
errSocketNotFound = portainer.Error("Unable to locate Unix socket")
)
// ParseFlags parse the CLI flags and return a portainer.Flags struct
func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
kingpin.Version(version)
flags := &portainer.CLIFlags{
Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(":9000").Short('p').String(),
Assets: kingpin.Flag("assets", "Path to the assets").Default(".").Short('a').String(),
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default("/data").Short('d').String(),
Endpoint: kingpin.Flag("host", "Dockerd endpoint").Default("unix:///var/run/docker.sock").Short('H').String(),
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
Swarm: kingpin.Flag("swarm", "Swarm cluster support").Default("false").Short('s').Bool(),
Templates: kingpin.Flag("templates", "URL to the templates (apps) definitions").Default("https://raw.githubusercontent.com/portainer/templates/master/templates.json").Short('t').String(),
TLSVerify: kingpin.Flag("tlsverify", "TLS support").Default("false").Bool(),
TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default("/certs/ca.pem").String(),
TLSCert: kingpin.Flag("tlscert", "Path to the TLS certificate file").Default("/certs/cert.pem").String(),
TLSKey: kingpin.Flag("tlskey", "Path to the TLS key").Default("/certs/key.pem").String(),
}
kingpin.Parse()
return flags, nil
}
// ValidateFlags validates the values of the flags.
func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
if !strings.HasPrefix(*flags.Endpoint, "unix://") && !strings.HasPrefix(*flags.Endpoint, "tcp://") {
return errInvalidEnpointProtocol
}
if strings.HasPrefix(*flags.Endpoint, "unix://") {
socketPath := strings.TrimPrefix(*flags.Endpoint, "unix://")
if _, err := os.Stat(socketPath); err != nil {
if os.IsNotExist(err) {
return errSocketNotFound
}
return err
}
}
return nil
}

View File

@ -1,46 +1,40 @@
package main
package cli
import (
"github.com/portainer/portainer"
"fmt"
"gopkg.in/alecthomas/kingpin.v2"
"strings"
)
// pair defines a key/value pair
type pair struct {
Name string `json:"name"`
Value string `json:"value"`
}
type pairList []portainer.Pair
// pairList defines an array of Label
type pairList []pair
// Set implementation for Labels
// Set implementation for a list of portainer.Pair
func (l *pairList) Set(value string) error {
parts := strings.SplitN(value, "=", 2)
if len(parts) != 2 {
return fmt.Errorf("expected NAME=VALUE got '%s'", value)
}
p := new(pair)
p := new(portainer.Pair)
p.Name = parts[0]
p.Value = parts[1]
*l = append(*l, *p)
return nil
}
// String implementation for Labels
// String implementation for a list of pair
func (l *pairList) String() string {
return ""
}
// IsCumulative implementation for Labels
// IsCumulative implementation for a list of pair
func (l *pairList) IsCumulative() bool {
return true
}
// LabelParser defines a custom parser for Labels flags
func pairs(s kingpin.Settings) (target *[]pair) {
target = new([]pair)
func pairs(s kingpin.Settings) (target *[]portainer.Pair) {
target = new([]portainer.Pair)
s.SetValue((*pairList)(target))
return
}

70
api/cmd/portainer/main.go Normal file
View File

@ -0,0 +1,70 @@
package main // import "github.com/portainer/portainer"
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt"
"github.com/portainer/portainer/cli"
"github.com/portainer/portainer/crypto"
"github.com/portainer/portainer/http"
"github.com/portainer/portainer/jwt"
"log"
)
func main() {
var cli portainer.CLIService = &cli.Service{}
flags, err := cli.ParseFlags(portainer.APIVersion)
if err != nil {
log.Fatal(err)
}
err = cli.ValidateFlags(flags)
if err != nil {
log.Fatal(err)
}
settings := &portainer.Settings{
Swarm: *flags.Swarm,
HiddenLabels: *flags.Labels,
Logo: *flags.Logo,
}
var store = bolt.NewStore(*flags.Data)
err = store.Open()
if err != nil {
log.Fatal(err)
}
defer store.Close()
jwtService, err := jwt.NewService()
if err != nil {
log.Fatal(err)
}
var cryptoService portainer.CryptoService = &crypto.Service{}
endpointConfiguration := &portainer.EndpointConfiguration{
Endpoint: *flags.Endpoint,
TLS: *flags.TLSVerify,
TLSCACertPath: *flags.TLSCacert,
TLSCertPath: *flags.TLSCert,
TLSKeyPath: *flags.TLSKey,
}
var server portainer.Server = &http.Server{
BindAddress: *flags.Addr,
AssetsPath: *flags.Assets,
Settings: settings,
TemplatesURL: *flags.Templates,
UserService: store.UserService,
CryptoService: cryptoService,
JWTService: jwtService,
EndpointConfig: endpointConfiguration,
}
log.Printf("Starting Portainer on %s", *flags.Addr)
err = server.Start()
if err != nil {
log.Fatal(err)
}
}

22
api/crypto/crypto.go Normal file
View File

@ -0,0 +1,22 @@
package crypto
import (
"golang.org/x/crypto/bcrypt"
)
// Service represents a service for encrypting/hashing data.
type Service struct{}
// Hash hashes a string using the bcrypt algorithm
func (*Service) Hash(data string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(data), bcrypt.DefaultCost)
if err != nil {
return "", nil
}
return string(hash), nil
}
// CompareHashAndData compares a hash to clear data and returns an error if the comparison fails.
func (*Service) CompareHashAndData(hash string, data string) error {
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(data))
}

View File

@ -1,98 +0,0 @@
package main
import (
"encoding/json"
"errors"
"github.com/boltdb/bolt"
)
const (
userBucketName = "users"
)
type (
dataStore struct {
db *bolt.DB
}
userItem struct {
Username string `json:"username"`
Password string `json:"password,omitempty"`
}
)
var (
errUserNotFound = errors.New("User not found")
)
func (dataStore *dataStore) initDataStore() error {
return dataStore.db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(userBucketName))
if err != nil {
return err
}
return nil
})
}
func (dataStore *dataStore) cleanUp() {
dataStore.db.Close()
}
func newDataStore(databasePath string) (*dataStore, error) {
db, err := bolt.Open(databasePath, 0600, nil)
if err != nil {
return nil, err
}
return &dataStore{
db: db,
}, nil
}
func (dataStore *dataStore) getUserByUsername(username string) (*userItem, error) {
var data []byte
err := dataStore.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
value := bucket.Get([]byte(username))
if value == nil {
return errUserNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return nil, err
}
var user userItem
err = json.Unmarshal(data, &user)
if err != nil {
return nil, err
}
return &user, nil
}
func (dataStore *dataStore) updateUser(user userItem) error {
buffer, err := json.Marshal(user)
if err != nil {
return err
}
err = dataStore.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
err = bucket.Put([]byte(user.Username), buffer)
if err != nil {
return err
}
return nil
})
if err != nil {
return err
}
return nil
}

28
api/errors.go Normal file
View File

@ -0,0 +1,28 @@
package portainer
// General errors.
const (
ErrUnauthorized = Error("Unauthorized")
)
// User errors.
const (
ErrUserNotFound = Error("User not found")
)
// Crypto errors.
const (
ErrCryptoHashFailure = Error("Unable to hash data")
)
// JWT errors.
const (
ErrSecretGeneration = Error("Unable to generate secret key")
ErrInvalidJWTToken = Error("Invalid JWT token")
)
// Error represents an application error.
type Error string
// Error returns the error message.
func (e Error) Error() string { return string(e) }

View File

@ -1,24 +0,0 @@
package main
import (
"golang.org/x/net/websocket"
"log"
)
// execContainer is used to create a websocket communication with an exec instance
func (a *api) execContainer(ws *websocket.Conn) {
qry := ws.Request().URL.Query()
execID := qry.Get("id")
var host string
if a.endpoint.Scheme == "tcp" {
host = a.endpoint.Host
} else if a.endpoint.Scheme == "unix" {
host = a.endpoint.Path
}
if err := hijack(host, a.endpoint.Scheme, "POST", "/exec/"+execID+"/start", a.tlsConfig, true, ws, ws, ws, nil, nil); err != nil {
log.Fatalf("error during hijack: %s", err)
return
}
}

View File

@ -1,92 +0,0 @@
package main
import (
"github.com/gorilla/mux"
"golang.org/x/net/websocket"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
)
// newHandler creates a new http.Handler
func (a *api) newHandler(settings *Settings) http.Handler {
var (
mux = mux.NewRouter()
fileHandler = http.FileServer(http.Dir(a.assetPath))
)
handler := a.newAPIHandler()
mux.Handle("/ws/exec", websocket.Handler(a.execContainer))
mux.HandleFunc("/auth", a.authHandler)
mux.Handle("/users", addMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
a.usersHandler(w, r)
}), a.authenticate, secureHeaders))
mux.Handle("/users/{username}", addMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
a.userHandler(w, r)
}), a.authenticate, secureHeaders))
mux.Handle("/users/{username}/passwd", addMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
a.userPasswordHandler(w, r)
}), a.authenticate, secureHeaders))
mux.HandleFunc("/users/admin/check", a.checkAdminHandler)
mux.HandleFunc("/users/admin/init", a.initAdminHandler)
mux.HandleFunc("/settings", func(w http.ResponseWriter, r *http.Request) {
settingsHandler(w, r, settings)
})
mux.HandleFunc("/templates", func(w http.ResponseWriter, r *http.Request) {
templatesHandler(w, r, a.templatesURL)
})
mux.PathPrefix("/dockerapi/").Handler(http.StripPrefix("/dockerapi", addMiddleware(handler, a.authenticate, secureHeaders)))
mux.PathPrefix("/").Handler(http.StripPrefix("/", fileHandler))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mux.ServeHTTP(w, r)
})
}
// newAPIHandler initializes a new http.Handler based on the URL scheme
func (a *api) newAPIHandler() http.Handler {
var handler http.Handler
var endpoint = *a.endpoint
if endpoint.Scheme == "tcp" {
if a.tlsConfig != nil {
handler = a.newTCPHandlerWithTLS(&endpoint)
} else {
handler = a.newTCPHandler(&endpoint)
}
} else if endpoint.Scheme == "unix" {
socketPath := endpoint.Path
if _, err := os.Stat(socketPath); err != nil {
if os.IsNotExist(err) {
log.Fatalf("Unix socket %s does not exist", socketPath)
}
log.Fatal(err)
}
handler = a.newUnixHandler(socketPath)
} else {
log.Fatalf("Bad Docker enpoint: %v. Only unix:// and tcp:// are supported.", &endpoint)
}
return handler
}
// newUnixHandler initializes a new UnixHandler
func (a *api) newUnixHandler(e string) http.Handler {
return &unixHandler{e}
}
// newTCPHandler initializes a HTTP reverse proxy
func (a *api) newTCPHandler(u *url.URL) http.Handler {
u.Scheme = "http"
return httputil.NewSingleHostReverseProxy(u)
}
// newTCPHandlerWithL initializes a HTTPS reverse proxy with a TLS configuration
func (a *api) newTCPHandlerWithTLS(u *url.URL) http.Handler {
u.Scheme = "https"
proxy := httputil.NewSingleHostReverseProxy(u)
proxy.Transport = &http.Transport{
TLSClientConfig: a.tlsConfig,
}
return proxy
}

95
api/http/auth_handler.go Normal file
View File

@ -0,0 +1,95 @@
package http
import (
"github.com/portainer/portainer"
"encoding/json"
"github.com/asaskevich/govalidator"
"github.com/gorilla/mux"
"log"
"net/http"
"os"
)
// AuthHandler represents an HTTP API handler for managing authentication.
type AuthHandler struct {
*mux.Router
Logger *log.Logger
UserService portainer.UserService
CryptoService portainer.CryptoService
JWTService portainer.JWTService
}
const (
// ErrInvalidCredentialsFormat is an error raised when credentials format is not valid
ErrInvalidCredentialsFormat = portainer.Error("Invalid credentials format")
// ErrInvalidCredentials is an error raised when credentials for a user are invalid
ErrInvalidCredentials = portainer.Error("Invalid credentials")
)
// NewAuthHandler returns a new instance of DialHandler.
func NewAuthHandler() *AuthHandler {
h := &AuthHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.HandleFunc("/auth", h.handlePostAuth)
return h
}
func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
handleNotAllowed(w, []string{"POST"})
return
}
var req postAuthRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
_, err := govalidator.ValidateStruct(req)
if err != nil {
Error(w, ErrInvalidCredentialsFormat, http.StatusBadRequest, handler.Logger)
return
}
var username = req.Username
var password = req.Password
u, err := handler.UserService.User(username)
if err == portainer.ErrUserNotFound {
Error(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
err = handler.CryptoService.CompareHashAndData(u.Password, password)
if err != nil {
Error(w, ErrInvalidCredentials, http.StatusUnprocessableEntity, handler.Logger)
return
}
tokenData := &portainer.TokenData{
username,
}
token, err := handler.JWTService.GenerateToken(tokenData)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
encodeJSON(w, &postAuthResponse{JWT: token}, handler.Logger)
}
type postAuthRequest struct {
Username string `valid:"alphanum,required"`
Password string `valid:"required"`
}
type postAuthResponse struct {
JWT string `json:"jwt"`
}

115
api/http/docker_handler.go Normal file
View File

@ -0,0 +1,115 @@
package http
import (
"github.com/portainer/portainer"
"github.com/gorilla/mux"
"io"
"log"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
)
// DockerHandler represents an HTTP API handler for proxying requests to the Docker API.
type DockerHandler struct {
*mux.Router
Logger *log.Logger
middleWareService *middleWareService
proxy http.Handler
}
// NewDockerHandler returns a new instance of DockerHandler.
func NewDockerHandler(middleWareService *middleWareService) *DockerHandler {
h := &DockerHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
middleWareService: middleWareService,
}
h.PathPrefix("/").Handler(middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.proxyRequestsToDockerAPI(w, r)
})))
return h
}
func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.Request) {
handler.proxy.ServeHTTP(w, r)
}
func (handler *DockerHandler) setupProxy(config *portainer.EndpointConfiguration) error {
var proxy http.Handler
endpointURL, err := url.Parse(config.Endpoint)
if err != nil {
return err
}
if endpointURL.Scheme == "tcp" {
if config.TLS {
proxy, err = newHTTPSProxy(endpointURL, config)
if err != nil {
return err
}
} else {
proxy = newHTTPProxy(endpointURL)
}
} else {
// Assume unix:// scheme
proxy = newSocketProxy(endpointURL.Path)
}
handler.proxy = proxy
return nil
}
func newHTTPProxy(u *url.URL) http.Handler {
u.Scheme = "http"
return httputil.NewSingleHostReverseProxy(u)
}
func newHTTPSProxy(u *url.URL, endpointConfig *portainer.EndpointConfiguration) (http.Handler, error) {
u.Scheme = "https"
proxy := httputil.NewSingleHostReverseProxy(u)
config, err := createTLSConfiguration(endpointConfig.TLSCACertPath, endpointConfig.TLSCertPath, endpointConfig.TLSKeyPath)
if err != nil {
return nil, err
}
proxy.Transport = &http.Transport{
TLSClientConfig: config,
}
return proxy, nil
}
func newSocketProxy(path string) http.Handler {
return &unixSocketHandler{path}
}
// unixSocketHandler represents a handler to proxy HTTP requests via a unix:// socket
type unixSocketHandler struct {
path string
}
func (h *unixSocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
conn, err := net.Dial("unix", h.path)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
c := httputil.NewClientConn(conn, nil)
defer c.Close()
res, err := c.Do(r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer res.Body.Close()
for k, vv := range res.Header {
for _, v := range vv {
w.Header().Add(k, v)
}
}
if _, err := io.Copy(w, res.Body); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}

76
api/http/handler.go Normal file
View File

@ -0,0 +1,76 @@
package http
import (
"github.com/portainer/portainer"
"encoding/json"
"log"
"net/http"
"strings"
)
// Handler is a collection of all the service handlers.
type Handler struct {
AuthHandler *AuthHandler
UserHandler *UserHandler
SettingsHandler *SettingsHandler
TemplatesHandler *TemplatesHandler
DockerHandler *DockerHandler
WebSocketHandler *WebSocketHandler
FileHandler http.Handler
}
const (
// ErrInvalidJSON defines an error raised the app is unable to parse request data
ErrInvalidJSON = portainer.Error("Invalid JSON")
// ErrInvalidRequestFormat defines an error raised when the format of the data sent in a request is not valid
ErrInvalidRequestFormat = portainer.Error("Invalid request data format")
)
// ServeHTTP delegates a request to the appropriate subhandler.
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/api/auth") {
http.StripPrefix("/api", h.AuthHandler).ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/api/users") {
http.StripPrefix("/api", h.UserHandler).ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/api/settings") {
http.StripPrefix("/api", h.SettingsHandler).ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/api/templates") {
http.StripPrefix("/api", h.TemplatesHandler).ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/api/websocket") {
http.StripPrefix("/api", h.WebSocketHandler).ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/api/docker") {
http.StripPrefix("/api/docker", h.DockerHandler).ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/") {
h.FileHandler.ServeHTTP(w, r)
}
}
// Error writes an API error message to the response and logger.
func Error(w http.ResponseWriter, err error, code int, logger *log.Logger) {
// Log error.
logger.Printf("http error: %s (code=%d)", err, code)
// Write generic error response.
w.WriteHeader(code)
json.NewEncoder(w).Encode(&errorResponse{Err: err.Error()})
}
// errorResponse is a generic response for sending a error.
type errorResponse struct {
Err string `json:"err,omitempty"`
}
// handleNotAllowed writes an API error message to the response and sets the Allow header.
func handleNotAllowed(w http.ResponseWriter, allowedMethods []string) {
w.Header().Set("Allow", strings.Join(allowedMethods, ", "))
w.WriteHeader(http.StatusMethodNotAllowed)
json.NewEncoder(w).Encode(&errorResponse{Err: http.StatusText(http.StatusMethodNotAllowed)})
}
// encodeJSON encodes v to w in JSON format. Error() is called if encoding fails.
func encodeJSON(w http.ResponseWriter, v interface{}, logger *log.Logger) {
if err := json.NewEncoder(w).Encode(v); err != nil {
Error(w, err, http.StatusInternalServerError, logger)
}
}

View File

@ -1,12 +1,17 @@
package main
package http
import (
"fmt"
"github.com/dgrijalva/jwt-go"
"github.com/portainer/portainer"
"net/http"
"strings"
)
// Service represents a service to manage HTTP middlewares
type middleWareService struct {
jwtService portainer.JWTService
}
func addMiddleware(h http.Handler, middleware ...func(http.Handler) http.Handler) http.Handler {
for _, mw := range middleware {
h = mw(h)
@ -14,13 +19,27 @@ func addMiddleware(h http.Handler, middleware ...func(http.Handler) http.Handler
return h
}
// authenticate provides Authentication middleware for handlers
func (api *api) authenticate(next http.Handler) http.Handler {
func (service *middleWareService) addMiddleWares(h http.Handler) http.Handler {
h = service.middleWareSecureHeaders(h)
h = service.middleWareAuthenticate(h)
return h
}
// middleWareAuthenticate provides secure headers middleware for handlers
func (*middleWareService) middleWareSecureHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("X-Content-Type-Options", "nosniff")
w.Header().Add("X-Frame-Options", "DENY")
next.ServeHTTP(w, r)
})
}
// middleWareAuthenticate provides Authentication middleware for handlers
func (service *middleWareService) middleWareAuthenticate(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var token string
// Get token from the Authorization header
// format: Authorization: Bearer
tokens, ok := r.Header["Authorization"]
if ok && len(tokens) >= 1 {
token = tokens[0]
@ -32,34 +51,13 @@ func (api *api) authenticate(next http.Handler) http.Handler {
return
}
parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
msg := fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
return nil, msg
}
return api.secret, nil
})
err := service.jwtService.VerifyToken(token)
if err != nil {
http.Error(w, "Invalid JWT token", http.StatusUnauthorized)
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
if parsedToken == nil || !parsedToken.Valid {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
// context.Set(r, "user", parsedToken)
next.ServeHTTP(w, r)
return
})
}
// SecureHeaders adds secure headers to the API
func secureHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("X-Content-Type-Options", "nosniff")
w.Header().Add("X-Frame-Options", "DENY")
next.ServeHTTP(w, r)
})
}

53
api/http/server.go Normal file
View File

@ -0,0 +1,53 @@
package http
import (
"github.com/portainer/portainer"
"net/http"
)
// Server implements the portainer.Server interface
type Server struct {
BindAddress string
AssetsPath string
UserService portainer.UserService
CryptoService portainer.CryptoService
JWTService portainer.JWTService
Settings *portainer.Settings
TemplatesURL string
EndpointConfig *portainer.EndpointConfiguration
}
// Start starts the HTTP server
func (server *Server) Start() error {
middleWareService := &middleWareService{
jwtService: server.JWTService,
}
var authHandler = NewAuthHandler()
authHandler.UserService = server.UserService
authHandler.CryptoService = server.CryptoService
authHandler.JWTService = server.JWTService
var userHandler = NewUserHandler(middleWareService)
userHandler.UserService = server.UserService
userHandler.CryptoService = server.CryptoService
var settingsHandler = NewSettingsHandler(middleWareService)
settingsHandler.settings = server.Settings
var templatesHandler = NewTemplatesHandler(middleWareService)
templatesHandler.templatesURL = server.TemplatesURL
var dockerHandler = NewDockerHandler(middleWareService)
dockerHandler.setupProxy(server.EndpointConfig)
var websocketHandler = NewWebSocketHandler()
websocketHandler.endpointConfiguration = server.EndpointConfig
var fileHandler = http.FileServer(http.Dir(server.AssetsPath))
handler := &Handler{
AuthHandler: authHandler,
UserHandler: userHandler,
SettingsHandler: settingsHandler,
TemplatesHandler: templatesHandler,
DockerHandler: dockerHandler,
WebSocketHandler: websocketHandler,
FileHandler: fileHandler,
}
return http.ListenAndServe(server.BindAddress, handler)
}

View File

@ -0,0 +1,39 @@
package http
import (
"github.com/portainer/portainer"
"github.com/gorilla/mux"
"log"
"net/http"
"os"
)
// SettingsHandler represents an HTTP API handler for managing settings.
type SettingsHandler struct {
*mux.Router
Logger *log.Logger
middleWareService *middleWareService
settings *portainer.Settings
}
// NewSettingsHandler returns a new instance of SettingsHandler.
func NewSettingsHandler(middleWareService *middleWareService) *SettingsHandler {
h := &SettingsHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
middleWareService: middleWareService,
}
h.HandleFunc("/settings", h.handleGetSettings)
return h
}
// handleGetSettings handles GET requests on /settings
func (handler *SettingsHandler) handleGetSettings(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
handleNotAllowed(w, []string{"GET"})
return
}
encodeJSON(w, handler.settings, handler.Logger)
}

View File

@ -0,0 +1,55 @@
package http
import (
"fmt"
"github.com/gorilla/mux"
"io/ioutil"
"log"
"net/http"
"os"
)
// TemplatesHandler represents an HTTP API handler for managing templates.
type TemplatesHandler struct {
*mux.Router
Logger *log.Logger
middleWareService *middleWareService
templatesURL string
}
// NewTemplatesHandler returns a new instance of TemplatesHandler.
func NewTemplatesHandler(middleWareService *middleWareService) *TemplatesHandler {
h := &TemplatesHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
middleWareService: middleWareService,
}
h.Handle("/templates", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.handleGetTemplates(w, r)
})))
return h
}
// handleGetTemplates handles GET requests on /templates
func (handler *TemplatesHandler) handleGetTemplates(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
handleNotAllowed(w, []string{"GET"})
return
}
resp, err := http.Get(handler.templatesURL)
if err != nil {
log.Print(err)
http.Error(w, fmt.Sprintf("Error making request to %s: %s", handler.templatesURL, err.Error()), http.StatusInternalServerError)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Print(err)
http.Error(w, "Error reading body from templates URL", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(body)
}

View File

@ -1,27 +1,26 @@
package main
package http
import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
"log"
)
// newTLSConfig initializes a tls.Config using a CA certificate, a certificate and a key
func newTLSConfig(caCertPath, certPath, keyPath string) *tls.Config {
// createTLSConfiguration initializes a tls.Config using a CA certificate, a certificate and a key
func createTLSConfiguration(caCertPath, certPath, keyPath string) (*tls.Config, error) {
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
log.Fatal(err)
return nil, err
}
caCert, err := ioutil.ReadFile(caCertPath)
if err != nil {
log.Fatal(err)
return nil, err
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
tlsConfig := &tls.Config{
config := &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: caCertPool,
}
return tlsConfig
return config, nil
}

247
api/http/user_handler.go Normal file
View File

@ -0,0 +1,247 @@
package http
import (
"github.com/portainer/portainer"
"encoding/json"
"github.com/asaskevich/govalidator"
"github.com/gorilla/mux"
"log"
"net/http"
"os"
)
// UserHandler represents an HTTP API handler for managing users.
type UserHandler struct {
*mux.Router
Logger *log.Logger
UserService portainer.UserService
CryptoService portainer.CryptoService
middleWareService *middleWareService
}
// NewUserHandler returns a new instance of UserHandler.
func NewUserHandler(middleWareService *middleWareService) *UserHandler {
h := &UserHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
middleWareService: middleWareService,
}
h.Handle("/users", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.handlePostUsers(w, r)
})))
h.Handle("/users/{username}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.handleGetUser(w, r)
}))).Methods("GET")
h.Handle("/users/{username}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.handlePutUser(w, r)
}))).Methods("PUT")
h.Handle("/users/{username}/passwd", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.handlePostUserPasswd(w, r)
})))
h.HandleFunc("/users/admin/check", h.handleGetAdminCheck)
h.HandleFunc("/users/admin/init", h.handlePostAdminInit)
return h
}
// handlePostUsers handles POST requests on /users
func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
handleNotAllowed(w, []string{"POST"})
return
}
var req postUsersRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
_, err := govalidator.ValidateStruct(req)
if err != nil {
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
user := &portainer.User{
Username: req.Username,
}
user.Password, err = handler.CryptoService.Hash(req.Password)
if err != nil {
Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
return
}
err = handler.UserService.UpdateUser(user)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
type postUsersRequest struct {
Username string `valid:"alphanum,required"`
Password string `valid:"required"`
}
// handlePostUserPasswd handles POST requests on /users/:username/passwd
func (handler *UserHandler) handlePostUserPasswd(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
handleNotAllowed(w, []string{"POST"})
return
}
vars := mux.Vars(r)
username := vars["username"]
var req postUserPasswdRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
_, err := govalidator.ValidateStruct(req)
if err != nil {
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
var password = req.Password
u, err := handler.UserService.User(username)
if err == portainer.ErrUserNotFound {
Error(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
valid := true
err = handler.CryptoService.CompareHashAndData(u.Password, password)
if err != nil {
valid = false
}
encodeJSON(w, &postUserPasswdResponse{Valid: valid}, handler.Logger)
}
type postUserPasswdRequest struct {
Password string `valid:"required"`
}
type postUserPasswdResponse struct {
Valid bool `json:"valid"`
}
// handleGetUser handles GET requests on /users/:username
func (handler *UserHandler) handleGetUser(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
username := vars["username"]
user, err := handler.UserService.User(username)
if err == portainer.ErrUserNotFound {
Error(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
user.Password = ""
encodeJSON(w, &user, handler.Logger)
}
// handlePutUser handles PUT requests on /users/:username
func (handler *UserHandler) handlePutUser(w http.ResponseWriter, r *http.Request) {
var req putUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
_, err := govalidator.ValidateStruct(req)
if err != nil {
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
user := &portainer.User{
Username: req.Username,
}
user.Password, err = handler.CryptoService.Hash(req.Password)
if err != nil {
Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
return
}
err = handler.UserService.UpdateUser(user)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
type putUserRequest struct {
Username string `valid:"alphanum,required"`
Password string `valid:"required"`
}
// handlePostAdminInit handles GET requests on /users/admin/check
func (handler *UserHandler) handleGetAdminCheck(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
handleNotAllowed(w, []string{"GET"})
return
}
user, err := handler.UserService.User("admin")
if err == portainer.ErrUserNotFound {
Error(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
user.Password = ""
encodeJSON(w, &user, handler.Logger)
}
// handlePostAdminInit handles POST requests on /users/admin/init
func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
handleNotAllowed(w, []string{"POST"})
return
}
var req postAdminInitRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
_, err := govalidator.ValidateStruct(req)
if err != nil {
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
user := &portainer.User{
Username: "admin",
}
user.Password, err = handler.CryptoService.Hash(req.Password)
if err != nil {
Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
return
}
err = handler.UserService.UpdateUser(user)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
type postAdminInitRequest struct {
Password string `valid:"required"`
}

View File

@ -1,17 +1,78 @@
package main
package http
import (
"github.com/portainer/portainer"
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"github.com/gorilla/mux"
"golang.org/x/net/websocket"
"io"
"log"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"time"
)
// WebSocketHandler represents an HTTP API handler for proxying requests to a web socket.
type WebSocketHandler struct {
*mux.Router
Logger *log.Logger
middleWareService *middleWareService
endpointConfiguration *portainer.EndpointConfiguration
}
// NewWebSocketHandler returns a new instance of WebSocketHandler.
func NewWebSocketHandler() *WebSocketHandler {
h := &WebSocketHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.Handle("/websocket/exec", websocket.Handler(h.webSocketDockerExec))
return h
}
func (handler *WebSocketHandler) webSocketDockerExec(ws *websocket.Conn) {
qry := ws.Request().URL.Query()
execID := qry.Get("id")
// Should not be managed here
endpoint, err := url.Parse(handler.endpointConfiguration.Endpoint)
if err != nil {
log.Fatalf("Unable to parse endpoint URL: %s", err)
return
}
var host string
if endpoint.Scheme == "tcp" {
host = endpoint.Host
} else if endpoint.Scheme == "unix" {
host = endpoint.Path
}
// Should not be managed here
var tlsConfig *tls.Config
if handler.endpointConfiguration.TLS {
tlsConfig, err = createTLSConfiguration(handler.endpointConfiguration.TLSCACertPath,
handler.endpointConfiguration.TLSCertPath,
handler.endpointConfiguration.TLSKeyPath)
if err != nil {
log.Fatalf("Unable to create TLS configuration: %s", err)
return
}
}
if err := hijack(host, endpoint.Scheme, "POST", "/exec/"+execID+"/start", tlsConfig, true, ws, ws, ws, nil, nil); err != nil {
log.Fatalf("error during hijack: %s", err)
return
}
}
type execConfig struct {
Tty bool
Detach bool

View File

@ -1,29 +0,0 @@
package main
import (
"github.com/dgrijalva/jwt-go"
"time"
)
type claims struct {
Username string `json:"username"`
jwt.StandardClaims
}
func (api *api) generateJWTToken(username string) (string, error) {
expireToken := time.Now().Add(time.Hour * 8).Unix()
claims := claims{
username,
jwt.StandardClaims{
ExpiresAt: expireToken,
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signedToken, err := token.SignedString(api.secret)
if err != nil {
return "", err
}
return signedToken, nil
}

66
api/jwt/jwt.go Normal file
View File

@ -0,0 +1,66 @@
package jwt
import (
"github.com/portainer/portainer"
"fmt"
"github.com/dgrijalva/jwt-go"
"github.com/gorilla/securecookie"
"time"
)
// Service represents a service for managing JWT tokens.
type Service struct {
secret []byte
}
type claims struct {
Username string `json:"username"`
jwt.StandardClaims
}
// NewService initializes a new service. It will generate a random key that will be used to sign JWT tokens.
func NewService() (*Service, error) {
secret := securecookie.GenerateRandomKey(32)
if secret == nil {
return nil, portainer.ErrSecretGeneration
}
service := &Service{
secret,
}
return service, nil
}
// GenerateToken generates a new JWT token.
func (service *Service) GenerateToken(data *portainer.TokenData) (string, error) {
expireToken := time.Now().Add(time.Hour * 8).Unix()
cl := claims{
data.Username,
jwt.StandardClaims{
ExpiresAt: expireToken,
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, cl)
signedToken, err := token.SignedString(service.secret)
if err != nil {
return "", err
}
return signedToken, nil
}
// VerifyToken parses a JWT token and verify its validity. It returns an error if token is invalid.
func (service *Service) VerifyToken(token string) error {
parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
msg := fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
return nil, msg
}
return service.secret, nil
})
if err != nil || parsedToken == nil || !parsedToken.Valid {
return portainer.ErrInvalidJWTToken
}
return nil
}

View File

@ -1,52 +0,0 @@
package main // import "github.com/portainer/portainer"
import (
"gopkg.in/alecthomas/kingpin.v2"
)
const (
// Version number of portainer API
Version = "1.10.2"
)
// main is the entry point of the program
func main() {
kingpin.Version(Version)
var (
endpoint = kingpin.Flag("host", "Dockerd endpoint").Default("unix:///var/run/docker.sock").Short('H').String()
addr = kingpin.Flag("bind", "Address and port to serve Portainer").Default(":9000").Short('p').String()
assets = kingpin.Flag("assets", "Path to the assets").Default(".").Short('a').String()
data = kingpin.Flag("data", "Path to the folder where the data is stored").Default("/data").Short('d').String()
tlsverify = kingpin.Flag("tlsverify", "TLS support").Default("false").Bool()
tlscacert = kingpin.Flag("tlscacert", "Path to the CA").Default("/certs/ca.pem").String()
tlscert = kingpin.Flag("tlscert", "Path to the TLS certificate file").Default("/certs/cert.pem").String()
tlskey = kingpin.Flag("tlskey", "Path to the TLS key").Default("/certs/key.pem").String()
swarm = kingpin.Flag("swarm", "Swarm cluster support").Default("false").Short('s').Bool()
labels = pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l'))
logo = kingpin.Flag("logo", "URL for the logo displayed in the UI").String()
templates = kingpin.Flag("templates", "URL to the templates (apps) definitions").Default("https://raw.githubusercontent.com/portainer/templates/master/templates.json").Short('t').String()
)
kingpin.Parse()
apiConfig := apiConfig{
Endpoint: *endpoint,
BindAddress: *addr,
AssetPath: *assets,
DataPath: *data,
SwarmSupport: *swarm,
TLSEnabled: *tlsverify,
TLSCACertPath: *tlscacert,
TLSCertPath: *tlscert,
TLSKeyPath: *tlskey,
TemplatesURL: *templates,
}
settings := &Settings{
Swarm: *swarm,
HiddenLabels: *labels,
Logo: *logo,
}
api := newAPI(apiConfig)
api.run(settings)
}

92
api/portainer.go Normal file
View File

@ -0,0 +1,92 @@
package portainer
type (
// Pair defines a key/value string pair
Pair struct {
Name string `json:"name"`
Value string `json:"value"`
}
// CLIFlags represents the available flags on the CLI.
CLIFlags struct {
Addr *string
Assets *string
Data *string
Endpoint *string
Labels *[]Pair
Logo *string
Swarm *bool
Templates *string
TLSVerify *bool
TLSCacert *string
TLSCert *string
TLSKey *string
}
// Settings represents Portainer settings.
Settings struct {
Swarm bool `json:"swarm"`
HiddenLabels []Pair `json:"hiddenLabels"`
Logo string `json:"logo"`
}
// User represent a user account.
User struct {
Username string `json:"username"`
Password string `json:"password,omitempty"`
}
// TokenData represents the data embedded in a JWT token.
TokenData struct {
Username string
}
// EndpointConfiguration represents the data required to connect to a Docker API endpoint.
EndpointConfiguration struct {
Endpoint string
TLS bool
TLSCACertPath string
TLSCertPath string
TLSKeyPath string
}
// CLIService represents a service for managing CLI.
CLIService interface {
ParseFlags(version string) (*CLIFlags, error)
ValidateFlags(flags *CLIFlags) error
}
// DataStore defines the interface to manage the data.
DataStore interface {
Open() error
Close() error
}
// Server defines the interface to serve the data.
Server interface {
Start() error
}
// UserService represents a service for managing users.
UserService interface {
User(username string) (*User, error)
UpdateUser(user *User) error
}
// CryptoService represents a service for encrypting/hashing data.
CryptoService interface {
Hash(data string) (string, error)
CompareHashAndData(hash string, data string) error
}
// JWTService represents a service for managing JWT tokens.
JWTService interface {
GenerateToken(data *TokenData) (string, error)
VerifyToken(token string) error
}
)
const (
// APIVersion is the version number of portainer API.
APIVersion = "1.10.2"
)

View File

@ -1,18 +0,0 @@
package main
import (
"encoding/json"
"net/http"
)
// Settings defines the settings available under the /settings endpoint
type Settings struct {
Swarm bool `json:"swarm"`
HiddenLabels pairList `json:"hiddenLabels"`
Logo string `json:"logo"`
}
// settingsHandler defines a handler function used to encode the configuration in JSON
func settingsHandler(w http.ResponseWriter, r *http.Request, s *Settings) {
json.NewEncoder(w).Encode(*s)
}

View File

@ -1,27 +0,0 @@
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
)
// templatesHandler defines a handler function used to retrieve the templates from a URL and put them in the response
func templatesHandler(w http.ResponseWriter, r *http.Request, templatesURL string) {
resp, err := http.Get(templatesURL)
if err != nil {
http.Error(w, fmt.Sprintf("Error making request to %s: %s", templatesURL, err.Error()), http.StatusInternalServerError)
log.Print(err)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
http.Error(w, "Error reading body from templates URL", http.StatusInternalServerError)
log.Print(err)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(body)
}

View File

@ -1,47 +0,0 @@
package main
import (
"io"
"log"
"net"
"net/http"
"net/http/httputil"
)
// unixHandler defines a handler holding the path to a socket under UNIX
type unixHandler struct {
path string
}
// ServeHTTP implementation for unixHandler
func (h *unixHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
conn, err := net.Dial("unix", h.path)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Println(err)
return
}
c := httputil.NewClientConn(conn, nil)
defer c.Close()
res, err := c.Do(r)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Println(err)
return
}
defer res.Body.Close()
copyHeader(w.Header(), res.Header)
if _, err := io.Copy(w, res.Body); err != nil {
log.Println(err)
}
}
func copyHeader(dst, src http.Header) {
for k, vv := range src {
for _, v := range vv {
dst.Add(k, v)
}
}
}

View File

@ -1,219 +0,0 @@
package main
import (
"encoding/json"
"github.com/gorilla/mux"
"io/ioutil"
"log"
"net/http"
)
type (
passwordCheckRequest struct {
Password string `json:"password"`
}
passwordCheckResponse struct {
Valid bool `json:"valid"`
}
initAdminRequest struct {
Password string `json:"password"`
}
)
// handle /users
// Allowed methods: POST
func (api *api) usersHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
w.Header().Set("Allow", "POST")
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Unable to parse request body", http.StatusBadRequest)
return
}
var user userItem
err = json.Unmarshal(body, &user)
if err != nil {
http.Error(w, "Unable to parse user data", http.StatusBadRequest)
return
}
user.Password, err = hashPassword(user.Password)
if err != nil {
http.Error(w, "Unable to hash user password", http.StatusInternalServerError)
return
}
err = api.dataStore.updateUser(user)
if err != nil {
log.Printf("Unable to persist user: %s", err.Error())
http.Error(w, "Unable to persist user", http.StatusInternalServerError)
return
}
}
// handle /users/admin/check
// Allowed methods: POST
func (api *api) checkAdminHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
w.Header().Set("Allow", "GET")
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
user, err := api.dataStore.getUserByUsername("admin")
if err == errUserNotFound {
log.Printf("User not found: %s", "admin")
http.Error(w, "User not found", http.StatusNotFound)
return
}
if err != nil {
log.Printf("Unable to retrieve user: %s", err.Error())
http.Error(w, "Unable to retrieve user", http.StatusInternalServerError)
return
}
user.Password = ""
json.NewEncoder(w).Encode(user)
}
// handle /users/admin/init
// Allowed methods: POST
func (api *api) initAdminHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
w.Header().Set("Allow", "POST")
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Unable to parse request body", http.StatusBadRequest)
return
}
var requestData initAdminRequest
err = json.Unmarshal(body, &requestData)
if err != nil {
http.Error(w, "Unable to parse user data", http.StatusBadRequest)
return
}
user := userItem{
Username: "admin",
}
user.Password, err = hashPassword(requestData.Password)
if err != nil {
http.Error(w, "Unable to hash user password", http.StatusInternalServerError)
return
}
err = api.dataStore.updateUser(user)
if err != nil {
log.Printf("Unable to persist user: %s", err.Error())
http.Error(w, "Unable to persist user", http.StatusInternalServerError)
return
}
}
// handle /users/{username}
// Allowed methods: PUT, GET
func (api *api) userHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "PUT" {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Unable to parse request body", http.StatusBadRequest)
return
}
var user userItem
err = json.Unmarshal(body, &user)
if err != nil {
http.Error(w, "Unable to parse user data", http.StatusBadRequest)
return
}
user.Password, err = hashPassword(user.Password)
if err != nil {
http.Error(w, "Unable to hash user password", http.StatusInternalServerError)
return
}
err = api.dataStore.updateUser(user)
if err != nil {
log.Printf("Unable to persist user: %s", err.Error())
http.Error(w, "Unable to persist user", http.StatusInternalServerError)
return
}
} else if r.Method == "GET" {
vars := mux.Vars(r)
username := vars["username"]
user, err := api.dataStore.getUserByUsername(username)
if err == errUserNotFound {
log.Printf("User not found: %s", username)
http.Error(w, "User not found", http.StatusNotFound)
return
}
if err != nil {
log.Printf("Unable to retrieve user: %s", err.Error())
http.Error(w, "Unable to retrieve user", http.StatusInternalServerError)
return
}
user.Password = ""
json.NewEncoder(w).Encode(user)
} else {
w.Header().Set("Allow", "PUT, GET")
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
}
// handle /users/{username}/passwd
// Allowed methods: POST
func (api *api) userPasswordHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
w.Header().Set("Allow", "POST")
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
vars := mux.Vars(r)
username := vars["username"]
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Unable to parse request body", http.StatusBadRequest)
return
}
var data passwordCheckRequest
err = json.Unmarshal(body, &data)
if err != nil {
http.Error(w, "Unable to parse user data", http.StatusBadRequest)
return
}
user, err := api.dataStore.getUserByUsername(username)
if err != nil {
log.Printf("Unable to retrieve user: %s", err.Error())
http.Error(w, "Unable to retrieve user", http.StatusInternalServerError)
return
}
valid := true
err = checkPasswordValidity(data.Password, user.Password)
if err != nil {
valid = false
}
response := passwordCheckResponse{
Valid: valid,
}
json.NewEncoder(w).Encode(response)
}

View File

@ -498,10 +498,11 @@ angular.module('portainer', [
}])
// This is your docker url that the api will use to make requests
// You need to set this to the api endpoint without the port i.e. http://192.168.1.9
.constant('DOCKER_ENDPOINT', 'dockerapi')
.constant('DOCKER_ENDPOINT', '/api/docker')
.constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is required. If you have a port, prefix it with a ':' i.e. :4243
.constant('CONFIG_ENDPOINT', 'settings')
.constant('AUTH_ENDPOINT', 'auth')
.constant('TEMPLATES_ENDPOINT', 'templates')
.constant('CONFIG_ENDPOINT', '/api/settings')
.constant('AUTH_ENDPOINT', '/api/auth')
.constant('USERS_ENDPOINT', '/api/users')
.constant('TEMPLATES_ENDPOINT', '/api/templates')
.constant('PAGINATION_MAX_ITEMS', 10)
.constant('UI_VERSION', 'v1.10.2');

View File

@ -55,7 +55,7 @@ function ($scope, $stateParams, Settings, Container, Image, Exec, $timeout, Mess
} else {
var execId = d.Id;
resizeTTY(execId, termHeight, termWidth);
var url = window.location.href.split('#')[0] + 'ws/exec?id=' + execId;
var url = window.location.href.split('#')[0] + 'api/websocket/exec?id=' + execId;
if (url.indexOf('https') > -1) {
url = url.replace('https://', 'wss://');
} else {

View File

@ -229,9 +229,9 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize'])
}
});
}])
.factory('Users', ['$resource', function UsersFactory($resource) {
.factory('Users', ['$resource', 'USERS_ENDPOINT', function UsersFactory($resource, USERS_ENDPOINT) {
'use strict';
return $resource('/users/:username/:action', {}, {
return $resource(USERS_ENDPOINT + '/:username/:action', {}, {
create: { method: 'POST' },
get: {method: 'GET', params: { username: '@username' } },
update: { method: 'PUT', params: { username: '@username' } },

View File

@ -297,34 +297,34 @@ module.exports = function (grunt) {
},
buildBinary: {
command: [
'docker run --rm -v $(pwd)/api:/src portainer/golang-builder',
'shasum api/portainer > portainer-checksum.txt',
'docker run --rm -v $(pwd)/api:/src portainer/golang-builder /src/cmd/portainer',
'shasum api/cmd/portainer/portainer > portainer-checksum.txt',
'mkdir -p dist',
'mv api/portainer dist/'
'mv api/cmd/portainer/portainer dist/'
].join(' && ')
},
buildUnixArmBinary: {
command: [
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="linux" -e BUILD_GOARCH="arm" portainer/golang-builder:cross-platform',
'shasum api/portainer-linux-arm > portainer-checksum.txt',
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="linux" -e BUILD_GOARCH="arm" portainer/golang-builder:cross-platform /src/cmd/portainer',
'shasum api/cmd/portainer/portainer-linux-arm > portainer-checksum.txt',
'mkdir -p dist',
'mv api/portainer-linux-arm dist/portainer'
'mv api/cmd/portainer/portainer-linux-arm dist/portainer'
].join(' && ')
},
buildDarwinBinary: {
command: [
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="darwin" -e BUILD_GOARCH="amd64" portainer/golang-builder:cross-platform',
'shasum api/portainer-darwin-amd64 > portainer-checksum.txt',
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="darwin" -e BUILD_GOARCH="amd64" portainer/golang-builder:cross-platform /src/cmd/portainer',
'shasum api/cmd/portainer/portainer-darwin-amd64 > portainer-checksum.txt',
'mkdir -p dist',
'mv api/portainer-darwin-amd64 dist/portainer'
'mv api/cmd/portainer/portainer-darwin-amd64 dist/portainer'
].join(' && ')
},
buildWindowsBinary: {
command: [
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="windows" -e BUILD_GOARCH="amd64" portainer/golang-builder:cross-platform',
'shasum api/portainer-windows-amd64 > portainer-checksum.txt',
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="windows" -e BUILD_GOARCH="amd64" portainer/golang-builder:cross-platform /src/cmd/portainer',
'shasum api/cmd/portainer/portainer-windows-amd64 > portainer-checksum.txt',
'mkdir -p dist',
'mv api/portainer-windows-amd64 dist/portainer.exe'
'mv api/cmd/portainer/portainer-windows-amd64 dist/portainer.exe'
].join(' && ')
},
run: {