perkeep/pkg/server/app/app.go

246 lines
8.0 KiB
Go

/*
Copyright 2014 The Camlistore Authors.
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 app helps with configuring and starting server applications
// from Camlistore.
package app
import (
"fmt"
"log"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"camlistore.org/pkg/auth"
camhttputil "camlistore.org/pkg/httputil"
"camlistore.org/pkg/jsonconfig"
)
// Handler acts as a reverse proxy for a server application started by
// Camlistore. It can also serve some extra JSON configuration to the app.
type Handler struct {
name string // Name of the app's program.
envVars map[string]string // Variables set in the app's process environment. See pkg/app/vars.txt.
auth auth.AuthMode // Used for basic HTTP authenticating against the app requests.
appConfig jsonconfig.Obj // Additional parameters the app can request, or nil.
proxy *httputil.ReverseProxy // For redirecting requests to the app.
backendURL string // URL that we proxy to (i.e. base URL of the app).
}
func (a *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if camhttputil.PathSuffix(req) == "config.json" {
if a.auth.AllowedAccess(req)&auth.OpGet == auth.OpGet {
camhttputil.ReturnJSON(rw, a.appConfig)
} else {
auth.SendUnauthorized(rw, req)
}
return
}
if a.proxy == nil {
http.Error(rw, "no proxy for the app", 500)
return
}
a.proxy.ServeHTTP(rw, req)
}
// randPortBackendURL picks a random free port to listen on, and combines it
// with apiHost and appHandlerPrefix to create the appBackendURL that the app
// will listen on, and that the app handler will proxy to.
func randPortBackendURL(apiHost, appHandlerPrefix string) (string, error) {
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
if err != nil {
return "", err
}
listener, err := net.ListenTCP("tcp", addr)
if err != nil {
return "", fmt.Errorf("could not listen to find random port: %v", err)
}
randAddr := listener.Addr().(*net.TCPAddr)
if err := listener.Close(); err != nil {
return "", fmt.Errorf("could not close random listener: %v", err)
}
// TODO(mpl): see if can use netutil.TCPAddress.
scheme := "https://"
noScheme := strings.TrimPrefix(apiHost, scheme)
if strings.HasPrefix(noScheme, "http://") {
scheme = "http://"
noScheme = strings.TrimPrefix(noScheme, scheme)
}
hostPortPrefix := strings.SplitN(noScheme, "/", 2)
if len(hostPortPrefix) != 2 {
return "", fmt.Errorf("invalid apiHost: %q (no trailing slash?)", apiHost)
}
var host string
if strings.Contains(hostPortPrefix[0], "]") {
// we've got some IPv6 probably
hostPort := strings.Split(hostPortPrefix[0], "]")
host = hostPort[0] + "]"
} else {
hostPort := strings.Split(hostPortPrefix[0], ":")
host = hostPort[0]
}
return fmt.Sprintf("%s%s:%d%s", scheme, host, randAddr.Port, appHandlerPrefix), nil
}
// NewHandler returns a Handler that proxies requests to an app. Start() on the
// Handler starts the app.
// The apiHost must end in a slash and is the camlistored API server for the app
// process to hit.
// The appHandlerPrefix is the URL path prefix on apiHost where the app is mounted.
// It must end in a slash, and be at minimum "/".
// The conf object has the following members, related to the vars described in
// doc/app-environment.txt:
// "program", string, required. File name of the app's program executable. Either
// an absolute path, or the name of a file located in CAMLI_APP_BINDIR or in PATH.
// "backendURL", string, optional. Automatic if absent. It sets CAMLI_APP_BACKEND_URL.
// "appConfig", object, optional. Additional configuration that the app can request from Camlistore.
func NewHandler(conf jsonconfig.Obj, apiHost, appHandlerPrefix string) (*Handler, error) {
// TODO: remove the appHandlerPrefix if/when we change where the app config JSON URL is made available.
name := conf.RequiredString("program")
backendURL := conf.OptionalString("backendURL", "")
appConfig := conf.OptionalObject("appConfig")
// TODO(mpl): add an auth token in the extra config of the dev server config,
// that the hello app can use to setup a status handler than only responds
// to requests with that token.
if err := conf.Validate(); err != nil {
return nil, err
}
if apiHost == "" {
return nil, fmt.Errorf("app: could not initialize Handler for %q: Camlistore apiHost is unknown", name)
}
if appHandlerPrefix == "" {
return nil, fmt.Errorf("app: could not initialize Handler for %q: empty appHandlerPrefix", name)
}
if backendURL == "" {
var err error
// If not specified in the conf, we're dynamically picking the port of the CAMLI_APP_BACKEND_URL
// now (instead of letting the app itself do it), because we need to know it in advance in order
// to set the app handler's proxy.
backendURL, err = randPortBackendURL(apiHost, appHandlerPrefix)
if err != nil {
return nil, err
}
}
username, password := auth.RandToken(20), auth.RandToken(20)
camliAuth := username + ":" + password
basicAuth := auth.NewBasicAuth(username, password)
envVars := map[string]string{
"CAMLI_API_HOST": apiHost,
"CAMLI_AUTH": camliAuth,
"CAMLI_APP_BACKEND_URL": backendURL,
}
if appConfig != nil {
envVars["CAMLI_APP_CONFIG_URL"] = apiHost + strings.TrimPrefix(appHandlerPrefix, "/") + "config.json"
}
proxyURL, err := url.Parse(backendURL)
if err != nil {
return nil, fmt.Errorf("could not parse backendURL %q: %v", backendURL, err)
}
return &Handler{
name: name,
envVars: envVars,
auth: basicAuth,
appConfig: appConfig,
proxy: httputil.NewSingleHostReverseProxy(proxyURL),
backendURL: backendURL,
}, nil
}
func (a *Handler) Start() error {
name := a.name
if name == "" {
return fmt.Errorf("invalid app name: %q", name)
}
var binPath string
var err error
if e := os.Getenv("CAMLI_APP_BINDIR"); e != "" {
binPath, err = exec.LookPath(filepath.Join(e, name))
if err != nil {
log.Printf("%q executable not found in %q", name, e)
}
}
if binPath == "" || err != nil {
binPath, err = exec.LookPath(name)
if err != nil {
return fmt.Errorf("%q executable not found in PATH.", name)
}
}
cmd := exec.Command(binPath)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// TODO(mpl): extract Env methods from dev/devcam/env.go to a util pkg and use them here.
newVars := make(map[string]string, len(a.envVars))
for k, v := range a.envVars {
newVars[k+"="] = v
}
env := os.Environ()
for pos, oldkv := range env {
for k, newVal := range newVars {
if strings.HasPrefix(oldkv, k) {
env[pos] = k + newVal
delete(newVars, k)
break
}
}
}
for k, v := range newVars {
env = append(env, k+v)
}
cmd.Env = env
if err := cmd.Start(); err != nil {
return fmt.Errorf("could not start app %v: %v", name, err)
}
return nil
}
// ProgramName returns the name of the app's binary. It may be a file name in
// CAMLI_APP_BINDIR or PATH, or an absolute path.
func (a *Handler) ProgramName() string {
return a.name
}
// AuthMode returns the app handler's auth mode, which is also the auth that the
// app's client will be configured with. This mode should be registered with
// the server's auth modes, for the app to have access to the server's resources.
func (a *Handler) AuthMode() auth.AuthMode {
return a.auth
}
// AppConfig returns the optional configuration parameters object that the app
// can request from the app handler. It can be nil.
func (a *Handler) AppConfig() map[string]interface{} {
return a.appConfig
}
// BackendURL returns the appBackendURL that the app handler will proxy to.
func (a *Handler) BackendURL() string {
return a.backendURL
}