initial package layout, first working parts

This commit is contained in:
Jan Halfar 2014-08-10 20:07:05 +02:00
commit 7dbb05bd74
15 changed files with 686 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.*
!.git*

35
README.md Normal file
View File

@ -0,0 +1,35 @@
# gofoomo
Gofoomo lets you use Go in your foomo project. It also lets you use php in your Go project.
We want to use Go, but it is not the right language for a everyone, who is using php. Php´s save and reload work cycle in combination with it´s dynamic character is a great fit especially when doing frontend work.
## Complementing your LAMP stack
Go is a much younger and cleaner stack than LAMP.
- Serve static files without bugging your prefork apache
- Keep slow connections away from your php processes (not implemented yet)
- Hijack foomo json rpc services methods
- Your code is also running the server, this puts you in a place, where you can solve problems, that you can not solve in php
- Go´s runtime model is pretty much the opposite of the php runtime model
- all requests vs one request per lifetime
- shared memory vs process and memory isolation
- one bug to kill them all vs one bug kills one request
- hard, but fast vs easy but slow
## Sitting in front of your foomo LAMP app with Go
Go or php? It is up to you, to decide which tool provides better solutions for your problem and who on your team will be more productive with php or Go.
## Hijacking json rpc calls
Gofoomo lets you intercept and implement calls to foomo json rpc services. In addition Foomo.Go gives you an infrastructure to generate golang structs for php value objects.
## Access foomo configurations
Gofoomo gives you access to foomo configurations from Go. Hint: if your php configuration objects are well annotated they are essentially value objects and corresponding structs can easily be generated with Foomo.Go.
## More to come, but not much more
We are going to add features, as we are going to need them. The focus is to have a simple interface between foomo and Go.

41
foomo/core/client.go Normal file
View File

@ -0,0 +1,41 @@
package core
import (
"encoding/json"
"github.com/foomo/gofoomo/foomo"
"io/ioutil"
"net/http"
"net/url"
)
func get(foomo *foomo.Foomo, path ...string) (data []byte, err error) {
callUrl := foomo.GetURLWithCredentialsForDefaultBasicAuthDomain()
encodedPath := ""
for _, pathEntry := range path {
encodedPath += "/" + url.QueryEscape(pathEntry)
}
resp, err := http.Get(callUrl + "/foomo/core.php" + encodedPath)
if err == nil {
// handle error
defer resp.Body.Close()
data, err = ioutil.ReadAll(resp.Body)
}
return data, err
}
func GetJSON(foomo *foomo.Foomo, target interface{}, path ...string) error {
data, err := get(foomo, path...)
if err == nil {
return json.Unmarshal(data, &target)
} else {
return err
}
}
func GetConfig(foomo *foomo.Foomo, target interface{}, moduleName string, configName string, domain string) (err error) {
if len(domain) == 0 {
return GetJSON(foomo, target, "config", moduleName, configName)
} else {
return GetJSON(foomo, target, "config", moduleName, configName, domain)
}
}

61
foomo/core/client_test.go Normal file
View File

@ -0,0 +1,61 @@
package core
import (
"encoding/json"
"github.com/foomo/gofoomo/foomo"
"testing"
)
type CoreConfig struct {
EnabledModules []string
AvailableModules []string
RootHttp string
buildNumber int64
}
var testFoomo *foomo.Foomo
func getTestFoomo() *foomo.Foomo {
if testFoomo == nil {
f, _ := foomo.NewFoomo("/Users/jan/vagrant/schild/www/schild", "test", "http://schild-local-test.bestbytes.net")
testFoomo = f
}
return testFoomo
}
func TestGet(t *testing.T) {
f := getTestFoomo()
data, err := get(f, "config", "Foomo", "Foomo.core")
if err != nil {
t.Fatal(err)
}
var jsonData interface{}
err = json.Unmarshal(data, &jsonData)
if err != nil {
t.Fatal(err)
}
}
func TestGetJSON(t *testing.T) {
f := getTestFoomo()
config := new(CoreConfig)
err := GetJSON(f, config, "config", "Foomo", "Foomo.core")
if err != nil {
t.Fatal(err)
}
if len(config.EnabledModules) < 1 {
t.Fatal("there must be at least Foomo enabled")
}
}
func TestGetConfig(t *testing.T) {
f := getTestFoomo()
config := new(CoreConfig)
err := GetConfig(f, config, "Foomo", "Foomo.core", "")
if err != nil {
t.Fatal(err)
}
if len(config.EnabledModules) < 1 {
t.Fatal("there must be at least Foomo enabled")
}
}

100
foomo/foomo.go Normal file
View File

@ -0,0 +1,100 @@
package foomo
import (
"crypto/sha1"
"encoding/base64"
"io/ioutil"
u "net/url"
"strings"
)
type Foomo struct {
Root string
RunMode string
URL *u.URL
basicAuthCredentials struct {
user string
password string
}
}
func NewFoomo(foomoDir string, runMode string, url string) (f *Foomo, err error) {
f, err = makeFoomo(foomoDir, runMode, url, true)
return
}
func makeFoomo(foomoDir string, runMode string, url string, init bool) (foomo *Foomo, err error) {
f := new(Foomo)
f.Root = foomoDir
f.URL, err = u.Parse(url)
f.RunMode = runMode
if init {
f.setupBasicAuthCredentials()
}
return f, err
}
func (f *Foomo) getBasicAuthFileContentsForDomain(domain string) string {
basicAuthFilename := f.GetBasicAuthFilename("default")
bytes, err := ioutil.ReadFile(basicAuthFilename)
if err != nil {
return ""
} else {
return string(bytes)
}
}
func (f *Foomo) setupBasicAuthCredentials() error {
f.basicAuthCredentials.user = "gofoomo"
f.basicAuthCredentials.password = makeToken(50)
return ioutil.WriteFile(f.GetBasicAuthFilename("default"), []byte(setBasicAuthForUserInBasicAuthFileContents(f.getBasicAuthFileContentsForDomain("default"), f.basicAuthCredentials.user, f.basicAuthCredentials.password)), 0644)
}
func setBasicAuthForUserInBasicAuthFileContents(basicAuthFileContents string, user string, password string) string {
newLines := make([]string, 0)
LineLoop:
for _, line := range strings.Split(basicAuthFileContents, "\n") {
lineParts := strings.Split(line, ":")
if len(lineParts) == 2 && lineParts[0] == user {
continue LineLoop
} else {
newLines = append(newLines, line)
}
}
s := sha1.New()
s.Write([]byte(password))
passwordSum := []byte(s.Sum(nil))
newLines = append(newLines, user+":{SHA}"+base64.StdEncoding.EncodeToString(passwordSum))
return strings.Join(newLines, "\n")
}
func (f *Foomo) GetURLWithCredentialsForDefaultBasicAuthDomain() string {
url, _ := u.Parse(f.URL.String())
url.User = u.UserPassword(f.basicAuthCredentials.user, f.basicAuthCredentials.password)
return url.String()
}
func (f *Foomo) GetBasicAuthCredentialsForDefaultBasicAuthDomain() (user string, password string) {
return f.basicAuthCredentials.user, f.basicAuthCredentials.password
}
func (f *Foomo) GetModuleDir(moduleName string, dir string) string {
return f.Root + "/modules/" + moduleName + "/" + dir
}
func (f *Foomo) GetVarDir() string {
return f.Root + "/var/" + f.RunMode
}
func (f *Foomo) GetModuleHtdocsDir(moduleName string) string {
return f.GetModuleDir(moduleName, "htdocs")
}
func (f *Foomo) GetModuleHtdocsVarDir(moduleName string) string {
return f.GetVarDir() + "/htdocs/modulesVar/" + moduleName
}
func (f *Foomo) GetBasicAuthFilename(domain string) string {
return f.GetVarDir() + "/basicAuth/" + domain
}

47
foomo/foomo_test.go Normal file
View File

@ -0,0 +1,47 @@
package foomo
import (
"strings"
"testing"
)
func TestSetBasicAuthForUserInBasicAuthFileContents(t *testing.T) {
ba := "foo:bar\ntest:gone\nhansi:toll"
newBa := setBasicAuthForUserInBasicAuthFileContents(ba, "test", "test")
if len(strings.Split(newBa, "\n")) != 3 {
t.Fatal("wrong line count")
}
}
func getTestFoomoForFSStuff() *Foomo {
f, _ := makeFoomo("/var/www/foomo", "test", "http://test.foomo", false)
return f
}
func assertStringsEqual(t *testing.T, topic string, expected string, actual string) {
if actual != expected {
t.Fatal(topic, "actual: ", actual, " != expected: ", expected)
}
}
func TestGetVarDir(t *testing.T) {
actual := getTestFoomoForFSStuff().GetVarDir()
expected := "/var/www/foomo/var/test"
assertStringsEqual(t, "var dir", expected, actual)
}
func TestGetModuleDir(t *testing.T) {
assertStringsEqual(t, "module dir", "/var/www/foomo/modules/Foomo/htdocs", getTestFoomoForFSStuff().GetModuleDir("Foomo", "htdocs"))
}
func TestGetModuleHtdocsDir(t *testing.T) {
assertStringsEqual(t, "module htdocs dir", "/var/www/foomo/modules/Foomo/htdocs", getTestFoomoForFSStuff().GetModuleHtdocsDir("Foomo"))
}
func TestGetModuleHtdocsVarDir(t *testing.T) {
assertStringsEqual(t, "module htdocs var dir", "/var/www/foomo/var/test/htdocs/modulesVar/Foomo", getTestFoomoForFSStuff().GetModuleHtdocsVarDir("Foomo"))
}
func TestGetBasicAuthFilename(t *testing.T) {
assertStringsEqual(t, "basic auth file", "/var/www/foomo/var/test/basicAuth/sepp", getTestFoomoForFSStuff().GetBasicAuthFilename("sepp"))
}

30
foomo/token.go Normal file
View File

@ -0,0 +1,30 @@
package foomo
import (
"crypto/rand"
"io"
)
func makeToken(length int) string {
chars := []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789#$&*-_")
token := make([]byte, length)
randomData := make([]byte, length+(length/4)) // storage for random bytes.
clen := byte(len(chars))
maxrb := byte(256 - (256 % len(chars)))
i := 0
for {
if _, err := io.ReadFull(rand.Reader, randomData); err != nil {
panic(err)
}
for _, c := range randomData {
if c >= maxrb {
continue
}
token[i] = chars[c%clen]
i++
if i == length {
return string(token)
}
}
}
}

13
gofoomo.go Normal file
View File

@ -0,0 +1,13 @@
// Integrate golang with the foomo php framework. Think of gophers riding elephants or
// maybe also think of gophers pulling toy elephants.
//
//
// Example:
//
// f := gofoomo.NewFoomo("/Users/jan/vagrant/schild/www/schild", "test")
// p := proxy.NewProxy(f, "http://schild-local-test.bestbytes.net")
// // the static files handler will keep requests to static files away from apache
// p.AddHandler(handler.NewStaticFiles(f))
// http.ListenAndServe(":8080", p)
//
package gofoomo

2
proxy/handler/handler.go Normal file
View File

@ -0,0 +1,2 @@
// Common handlers for the gofoomo.proxy.
package handler

116
proxy/handler/rpc.go Normal file
View File

@ -0,0 +1,116 @@
package handler
import (
"encoding/json"
"github.com/foomo/gofoomo/rpc"
"io/ioutil"
"net/http"
"net/url"
"reflect"
"strings"
)
// This handler helps you to hijack foomo rpc services. Actually it is even
// better, you can hijack them method by method.
//
// f := gofoomo.NewFoomo("/var/www/myApp", "test")
// p := proxy.NewProxy(f, "http://test.myapp")
// service := NewFooService()
// rpcHandler := handler.NewRPC(service, "/foomo/modules/MyModule/services/foo.php")
// f.AddHandler(rpcHandler)
//
// Happy service hijacking!
type RPC struct {
path string
serviceObject interface{}
}
func NewRPC(serviceObject interface{}, path string) *RPC {
rpc := new(RPC)
rpc.path = path
rpc.serviceObject = serviceObject
return rpc
}
func (r *RPC) getApplicationPath(path string) string {
return path[len(r.path+"/Foomo.Services.RPC/serve")+1:]
}
func (r *RPC) getMethodFromPath(path string) string {
parts := strings.Split(r.getApplicationPath(path), "/")
if len(parts) > 0 {
return strings.ToUpper(parts[0][0:1]) + parts[0][1:]
} else {
return ""
}
}
func (r *RPC) handlesMethod(methodName string) bool {
return reflect.ValueOf(r.serviceObject).MethodByName(methodName).IsValid()
}
func (r *RPC) handlesPath(path string) bool {
return strings.HasPrefix(path, r.path) && r.handlesMethod(r.getMethodFromPath(path))
}
func (r *RPC) HandlesRequest(incomingRequest *http.Request) bool {
return incomingRequest.Method == "POST" && r.handlesPath(incomingRequest.URL.Path)
}
func (r *RPC) callServiceObjectWithHTTPRequest(incomingRequest *http.Request) (reply *rpc.MethodReply) {
reply = &rpc.MethodReply{}
path := incomingRequest.URL.Path
argumentMap := extractPostData(incomingRequest)
methodName := r.getMethodFromPath(path)
arguments := r.extractArguments(path)
r.callServiceObject(methodName, arguments, argumentMap, reply)
return reply
}
func (r *RPC) extractArguments(path string) (args []string) {
for _, value := range strings.Split(r.getApplicationPath(path), "/")[1:] {
unescapedArg, err := url.QueryUnescape(value)
if err != nil {
panic(err)
}
args = append(args, unescapedArg)
}
return args
}
func (r *RPC) callServiceObject(methodName string, arguments []string, argumentMap map[string]interface{}, reply *rpc.MethodReply) {
reflectionArgs := []reflect.Value{}
reflectionArgs = append(reflectionArgs, reflect.ValueOf(arguments), reflect.ValueOf(argumentMap), reflect.ValueOf(reply))
reflect.ValueOf(r.serviceObject).MethodByName(methodName).Call(reflectionArgs)
}
func extractPostData(incomingRequest *http.Request) map[string]interface{} {
body, err := ioutil.ReadAll(incomingRequest.Body)
if err != nil {
panic(err)
}
return jsonDecode(body).(map[string]interface{})
}
func jsonDecode(jsonData []byte) (data interface{}) {
err := json.Unmarshal(jsonData, &data)
if err != nil {
panic(err)
} else {
return data
}
}
func jsonEncode(data interface{}) []byte {
b, err := json.Marshal(data)
if err != nil {
panic(err)
} else {
return b
}
}
func (r *RPC) ServeHTTP(w http.ResponseWriter, incomingRequest *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write(jsonEncode(r.callServiceObjectWithHTTPRequest(incomingRequest)))
}

79
proxy/handler/rpc_test.go Normal file
View File

@ -0,0 +1,79 @@
package handler
import (
"github.com/foomo/gofoomo/rpc"
"log"
"testing"
)
type TestService struct {
}
func NewTestService() *TestService {
t := new(TestService)
return t
}
func getTestRPC() *RPC {
return NewRPC(NewTestService(), "/services/test.php")
}
func (t *TestService) Test(arguments []string, argumentMap map[string]interface{}, reply *rpc.MethodReply) {
reply.Value = true
}
func TestHandlesMethod(t *testing.T) {
r := getTestRPC()
if r.handlesMethod("Test") == false {
t.Fail()
}
if r.handlesMethod("testi") == true {
t.Fail()
}
}
func TestGetApplicationPath(t *testing.T) {
p := getTestRPC().getApplicationPath("/services/test.php/Foomo.Services.RPC/serve/test")
if p != "test" {
t.Fatal("i do not like this path", p)
}
}
func TestHandlesPath(t *testing.T) {
r := getTestRPC()
if r.handlesPath("/services/test.php/Foomo.Services.RPC/serve/test") == false {
t.Fatal("/services/test.php/Foomo.Services.RPC/serve/test")
}
if r.handlesPath("/services/test.php/Foomo.Services.RPC/serve/test/foo") == false {
t.Fatal("/services/test.php/Foomo.Services.RPC/serve/test/foo")
}
if r.handlesPath("/services/test.php/Foomo.Services.RPC/serve/testi/foo") == true {
t.Fatal("/services/test.php/Foomo.Services.RPC/serve/testi/foo")
}
}
func TestExtractArguments(t *testing.T) {
r := getTestRPC()
args := r.extractArguments("/services/test.php/Foomo.Services.RPC/serve/test/%C3%BCb%C3%A4l/B%C3%A4r")
if len(args) != 2 {
t.Fatal("wrong args length", args)
}
if args[0] != "übäl" {
t.Fatal("no übäl")
}
if args[1] != "Bär" {
t.Fatal("where is the bear")
}
}
func TestCallServiceObject(t *testing.T) {
r := getTestRPC()
var argumentMap map[string]interface{}
var arguments []string
reply := &rpc.MethodReply{}
r.callServiceObject("Test", arguments, argumentMap, reply)
if reply.Value != true {
log.Println(reply)
t.Fail()
}
}

View File

@ -0,0 +1,87 @@
package handler
import (
"github.com/foomo/gofoomo/foomo"
"io"
"net/http"
"os"
"strings"
)
// Handles serving static files from the local file system. It knows about
// foomos hierarchy and serves files from the htdocs directories of modules.
// Currently it will also serve files of disabled modules.
type StaticFiles struct {
foomo *foomo.Foomo
}
func NewStaticFiles(foomo *foomo.Foomo) *StaticFiles {
sf := new(StaticFiles)
sf.foomo = foomo
return sf
}
func (files *StaticFiles) HandlesRequest(incomingRequest *http.Request) bool {
if strings.HasPrefix(incomingRequest.URL.Path, "/foomo/modules/") {
parts := strings.Split(incomingRequest.URL.Path, "/")
if len(parts) > 3 {
moduleNameParts := strings.Split(parts[3], "-")
return fileExists(files.foomo.GetModuleHtdocsDir(moduleNameParts[0]) + "/" + strings.Join(parts[4:], "/"))
} else {
return false
}
} else if strings.HasPrefix(incomingRequest.URL.Path, "/foomo/modulesVar/") {
return true
} else {
return false
}
}
func fileExists(filename string) bool {
_, err := os.Stat(filename)
return err == nil
}
func (files *StaticFiles) ServeHTTP(w http.ResponseWriter, incomingRequest *http.Request) {
parts := strings.Split(incomingRequest.URL.Path, "/")
path := strings.Join(parts[4:], "/")
moduleNameParts := strings.Split(parts[3], "-")
moduleName := moduleNameParts[0]
var moduleDir string
if strings.HasPrefix(incomingRequest.URL.Path, "/foomo/modules/") {
moduleDir = files.foomo.GetModuleHtdocsDir(moduleName)
} else {
moduleDir = files.foomo.GetModuleHtdocsVarDir(moduleName)
}
f, err := os.Open(moduleDir + "/" + path)
if err != nil {
panic(err)
} else {
defer f.Close()
w.Header().Set("Content-Type", getContentType(path))
io.Copy(w, f)
}
}
func getContentType(path string) string {
if strings.HasSuffix(path, ".png") {
return "image/png"
} else if strings.HasSuffix(path, ".jpg") {
return "image/jpeg"
} else if strings.HasSuffix(path, ".jpeg") {
return "image/jpeg"
} else if strings.HasSuffix(path, ".gif") {
return "image/gif"
} else if strings.HasSuffix(path, ".css") {
return "text/css"
} else if strings.HasSuffix(path, ".js") {
return "application/javascript"
} else if strings.HasSuffix(path, ".html") {
return "text/html"
} else if strings.HasSuffix(path, ".") {
return ""
} else {
return "octet/stream"
}
}

41
proxy/proxy.go Normal file
View File

@ -0,0 +1,41 @@
package proxy
import (
"github.com/foomo/gofoomo/foomo"
"net/http"
"net/http/httputil"
)
type Handler interface {
HandlesRequest(incomingRequest *http.Request) bool
ServeHTTP(w http.ResponseWriter, incomingRequest *http.Request)
}
type Proxy struct {
foomo *foomo.Foomo
reverseProxy *httputil.ReverseProxy
handlers []Handler
}
func NewProxy(f *foomo.Foomo) *Proxy {
proxy := new(Proxy)
proxy.foomo = f
proxy.reverseProxy = httputil.NewSingleHostReverseProxy(proxy.foomo.URL)
return proxy
}
func (proxy *Proxy) ServeHTTP(w http.ResponseWriter, incomingRequest *http.Request) {
for _, handler := range proxy.handlers {
if handler.HandlesRequest(incomingRequest) {
handler.ServeHTTP(w, incomingRequest)
return
}
}
incomingRequest.Host = proxy.foomo.URL.Host
incomingRequest.URL.Opaque = incomingRequest.RequestURI
proxy.reverseProxy.ServeHTTP(w, incomingRequest)
}
func (proxy *Proxy) AddHandler(handler Handler) {
proxy.handlers = append(proxy.handlers, handler)
}

1
rpc/rpc.go Normal file
View File

@ -0,0 +1 @@
package rpc

30
rpc/value_objects.go Normal file
View File

@ -0,0 +1,30 @@
package rpc
// from php class Foomo\Services\RPC\Protocol\Call\MethodCall
// serializing a method call
type MethodCall struct {
// id of the method call
Id string `json:"id"`
// name of the method to be called
Method string `json:"method"`
// the method call arguments
Arguments []struct {
Name string `json:"name"`
Value interface{} `json:"value"`
} `json:"arguments"`
}
// from php class Foomo\Services\RPC\Protocol\Reply\MethodReply
// reply to a method call
type MethodReply struct {
// id of the method call
Id string `json:"id"`
// return value
Value interface{} `json:"value"`
// server side exception
Exception interface{} `json:"exception"`
// messages from the server
// possibly many of them
// possibly many types
Messages interface{} `json:"messages"`
}