diff --git a/.gitignore b/.gitignore index 5ce2595..63276d7 100644 --- a/.gitignore +++ b/.gitignore @@ -148,7 +148,6 @@ dist .pnp.* torima -proxy sqlite3.db test.sqlite3 secret.env @@ -157,7 +156,8 @@ private.der .gitignore cert.der -example/main +serv/serv *.db tmp + diff --git a/README.md b/README.md index 6df1090..38317e9 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ default_origin: app:5000 # your front-end server protection_scope: - api:5001 # your API servers -white_list_path: +skip_auth_list: - /favicon.ico scheme: http diff --git a/config.yaml b/config.yaml index b3b800d..86927c1 100644 --- a/config.yaml +++ b/config.yaml @@ -1,6 +1,6 @@ port: 8080 -white_list_path: +skip_auth_list: - /favicon.ico # default_origin: 127.0.0.1:8080 diff --git a/core/config.go b/core/config.go index 15e8954..23027cb 100644 --- a/core/config.go +++ b/core/config.go @@ -1,8 +1,6 @@ package core import ( - "fmt" - "log" "os" "github.com/creasty/defaults" @@ -14,12 +12,12 @@ type TorimaConfig struct { Host string `yaml:"host" default:"http://127.0.0.1:8080"` Port int `yaml:"port" default:"8080" ` Scheme string `yaml:"scheme" default:"http"` - WhiteListPath []string `yaml:"white_list_path" default:"[]"` + SkipAuthList []string `yaml:"skip_auth_list" default:"[]"` ProtectionScope []string `yaml:"protection_scope" default:"[]"` WebRoot string `yaml:"web_root" default:"/torima"` } -func readConfig() (*TorimaConfig, error) { +func ReadConfig() (*TorimaConfig, error) { var m TorimaConfig var def TorimaConfig // default config @@ -44,34 +42,3 @@ func readConfig() (*TorimaConfig, error) { return &m, err } - -func printConfig(config *TorimaConfig) { - fmt.Println("default_origin:", config.DefaultOrigin) - fmt.Println("host:", config.Host) - fmt.Println("port:", config.Port) - fmt.Println("scheme:", config.Scheme) - fmt.Println("white_list_path:", config.WhiteListPath) - fmt.Println("protection_scope:", config.ProtectionScope) - fmt.Println("web_root:", config.WebRoot) -} - -func readEnv(name, def string) string { - value := os.Getenv(name) - - if value == "" { - fmt.Printf("environment variable '%v' is not found so that proxy use '%v'\n", name, def) - value = def - } - - return value -} - -func readEnvOrPanic(name string) string { - value := os.Getenv(name) - - if value == "" { - log.Fatalf("environment variable '%v' is not found", name) - } - - return value -} diff --git a/core/core.go b/core/core.go index 266978e..bd53c56 100644 --- a/core/core.go +++ b/core/core.go @@ -7,13 +7,38 @@ import ( "github.com/gin-gonic/gin" ) -type TorimaDirector = func(proxy *TorimaProxy, req *http.Request, c *gin.Context) (bool, error) -type TorimaModifyResponse = func(proxy *TorimaProxy, req *http.Response, c *gin.Context) (bool, error) +type TorimaPackageStatus = int + +const ( + AuthNeeded TorimaPackageStatus = iota + Authed + NoAuthNeeded + ForceStop + Keep +) + +type TorimaPackageTarget interface{ *http.Request | *http.Response } + +type TorimaPackageContext[T TorimaPackageTarget] struct { + Proxy *TorimaProxy + Target T + GinContext *gin.Context + PackageStatus TorimaPackageStatus +} + +type TorimaDirectorPackageContext = TorimaPackageContext[*http.Request] +type TorimaModifyResponsePackageContext = TorimaPackageContext[*http.Response] + +type TorimaDirector func(*TorimaDirectorPackageContext) (TorimaPackageStatus, error) +type TorimaModifyResponse func(*TorimaModifyResponsePackageContext) (TorimaPackageStatus, error) +type TorimaDirectors []func(*TorimaDirectorPackageContext) (TorimaPackageStatus, error) +type TorimaModifyResponses []func(*TorimaModifyResponsePackageContext) (TorimaPackageStatus, error) + type TorimaProxyWebPage = func(proxy *TorimaProxy, c *gin.RouterGroup) type TorimaProxy struct { - Directors []TorimaDirector - ModifyResponses []TorimaModifyResponse + Directors TorimaDirectors + ModifyResponses TorimaModifyResponses ProxyWebPages []TorimaProxyWebPage Engine *gin.Engine Database *Database @@ -24,8 +49,8 @@ type TorimaProxy struct { func NewOchancoProxy( r *gin.Engine, - directors []TorimaDirector, - modifyResponses []TorimaModifyResponse, + directors TorimaDirectors, + modifyResponses TorimaModifyResponses, proxyWebPages []TorimaProxyWebPage, config *TorimaConfig, database *Database, diff --git a/core/director.go b/core/director.go deleted file mode 100644 index caacbe1..0000000 --- a/core/director.go +++ /dev/null @@ -1,149 +0,0 @@ -package core - -import ( - "bytes" - "fmt" - "net/http" - "net/http/httputil" - "strings" - - "github.com/ochanoco/ninsho" - gin_ninsho "github.com/ochanoco/ninsho/extension/gin" - - "github.com/gin-gonic/gin" - "golang.org/x/exp/slices" -) - -func RouteDirector(host string, proxy *TorimaProxy, req *http.Request, c *gin.Context) (bool, error) { - req.URL.Host = host - - // just to be sure - req.Header.Del("X-Torima-Proxy-Token") - req.Header.Set("X-Torima-Proxy-Token", SECRET) - - req.URL.Scheme = proxy.Config.Scheme - - return CONTINUE, nil -} - -func DefaultRouteDirector(proxy *TorimaProxy, req *http.Request, c *gin.Context) (bool, error) { - if strings.HasPrefix(req.URL.Path, "/torima/") { - return CONTINUE, nil - } - - host := proxy.Config.DefaultOrigin - - if host == "" { - err := fmt.Errorf("failed to get destination config (%s)", host) - return FINISHED, err - } - - return RouteDirector(host, proxy, req, c) -} - -func ThirdPartyDirector(proxy *TorimaProxy, req *http.Request, c *gin.Context) (bool, error) { - path := strings.Split(req.URL.Path, "/") - hasRedirectPrefix := strings.HasPrefix(req.URL.Path, "/torima/redirect/") - - if !hasRedirectPrefix || len(path) < 3 { - return CONTINUE, nil - } - - for _, origin := range proxy.Config.ProtectionScope { - if origin == path[3] { - req.Host = origin - req.URL.Host = origin - - p := strings.Join(path[4:], "/") - req.URL.Path = "/" + p - - req.URL.Scheme = "https" - return RouteDirector(origin, proxy, req, c) - } - } - - return CONTINUE, nil -} - -func SanitizeHeaderDirector(proxy *TorimaProxy, req *http.Request, c *gin.Context) (bool, error) { - headers := http.Header{ - "Host": {proxy.Config.Host}, - "User-Agent": {"torima"}, - - "Content-Type": req.Header["Content-Type"], - "Content-Length": req.Header["Content-Length"], - - "Accept": req.Header["Accept"], - "Connection": req.Header["Connection"], - - "Accept-Encoding": req.Header["Accept-Encoding"], - "Accept-Language": req.Header["Accept-Language"], - - "Cookie": req.Header["Cookie"], - } - - req.Header = headers - - return CONTINUE, nil - -} - -func AuthDirector(proxy *TorimaProxy, req *http.Request, c *gin.Context) (bool, error) { - user, err := gin_ninsho.LoadUser[ninsho.LINE_USER](c) - - // just to be sure - req.Header.Del("X-Torima-UserID") - - if err != nil { - err = makeError(err, "failed to get user from session: ") - return FINISHED, err - } - - if user != nil { - req.Header.Set("X-Torima-UserID", user.Sub) - return CONTINUE, nil - } - - if req.Method == "GET" && req.URL.RawQuery == "" { - if req.URL.Path == "/" { - return CONTINUE, nil - } - - if slices.Contains(proxy.Config.WhiteListPath, req.URL.Path) { - return CONTINUE, nil - } - } - - return FINISHED, makeError(fmt.Errorf(""), unauthorizedErrorTag) -} - -func MakeLogDirector(flag string) TorimaDirector { - return func(proxy *TorimaProxy, req *http.Request, c *gin.Context) (bool, error) { - request, err := httputil.DumpRequest(req, true) - - if err != nil { - err = makeError(err, "failed to dump headers to json: ") - return FINISHED, err - } - - splited := bytes.Split(request, []byte("\r\n\r\n")) - - header := splited[0] - headerLen := len(header) - - body := request[headerLen:] - - l := proxy.Database.CreateRequestLog(string(header), body, flag) - _, err = l.Save(proxy.Database.Ctx) - - if err != nil { - err = makeError(err, "failed to save request: ") - return FINISHED, err - } - - return CONTINUE, err - } -} - -var BeforeLogDirector = MakeLogDirector("before") -var AfterLogDirector = MakeLogDirector("after") diff --git a/core/director_test.go b/core/director_test.go deleted file mode 100644 index bfd77be..0000000 --- a/core/director_test.go +++ /dev/null @@ -1,239 +0,0 @@ -package core - -import ( - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "net/url" - "os" - "testing" - - "github.com/gin-contrib/sessions" - "github.com/gin-contrib/sessions/cookie" - "github.com/gin-gonic/gin" - "github.com/ochanoco/ninsho" - "github.com/stretchr/testify/assert" -) - -func directorSample(t *testing.T) (*http.Request, *TestResponseRecorder, *gin.Context, *TorimaProxy) { - DB_TYPE = "sqlite3" - DB_CONFIG = "../data/test.db?_fk=1" - SECRET = "test_secret" - - recorder := CreateTestResponseRecorder() - context, r := gin.CreateTestContext(recorder) - - store := cookie.NewStore([]byte("test")) - r.Use(sessions.Sessions("torima-session", store)) - - db, err := InitDB(DB_CONFIG) - assert.NoError(t, err) - - config, file, err := readTestConfig(t) - assert.NoError(t, err) - defer os.Remove(file.Name()) - - proxy := NewOchancoProxy(r, DEFAULT_DIRECTORS, DEFAULT_MODIFY_RESPONSES, DEFAULT_PROXYWEB_PAGES, config, db) - req := httptest.NewRequest("GET", "http://localhost:8080/", nil) - - return req, recorder, context, &proxy -} - -func setupMockServer(handler http.HandlerFunc, req *http.Request, t *testing.T) (*httptest.Server, *url.URL) { - h := http.HandlerFunc(handler) - - ts := httptest.NewServer(h) - u, err := url.Parse(ts.URL) - assert.NoError(t, err) - - req.URL.Path = "/hello" - req.URL.Host = u.Host - req.Host = u.Host - - return ts, u -} - -// test for RouteDirector -func TestRouteDirector(t *testing.T) { - req, _, context, proxy := directorSample(t) - c, err := RouteDirector("example.com", proxy, req, context) - - assert.NoError(t, err) - assert.Equal(t, CONTINUE, c) - assert.Equal(t, "example.com", req.URL.Host) - assert.Equal(t, "http", req.URL.Scheme) - assert.Equal(t, SECRET, req.Header.Get("X-Torima-Proxy-Token")) -} - -// test for DefaultRouteDirector -func TestThirdPartyDirector(t *testing.T) { - h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "Hello, client") - }) - - ts := httptest.NewServer(h) - defer ts.Close() - - u, err := url.Parse(ts.URL) - assert.NoError(t, err) - - host := fmt.Sprintf("%v:%v", u.Host, u.Port()) - - req, _, context, proxy := directorSample(t) - - req.URL.Path = "/torima/redirect/" + host - - proxy.Config.ProtectionScope = []string{host} - - c, err := ThirdPartyDirector(proxy, req, context) - assert.NoError(t, err) - assert.Equal(t, CONTINUE, c) - - c, err = ThirdPartyDirector(proxy, req, context) - assert.NoError(t, err) - - assert.Equal(t, CONTINUE, c) - assert.Equal(t, host, req.URL.Host) -} - -// test for DefaultRouteDirector -func TestThirdPartyDirectorNoParmit(t *testing.T) { - unpermitHost := "not-in-list.example.com" - - req, _, context, proxy := directorSample(t) - - req.URL.Path = "/torima/redirect/" + unpermitHost + "/" - - c, err := ThirdPartyDirector(proxy, req, context) - assert.NoError(t, err) - assert.Equal(t, CONTINUE, c) - - c, err = ThirdPartyDirector(proxy, req, context) - assert.NoError(t, err) - - assert.Equal(t, CONTINUE, c) - assert.NotEqual(t, unpermitHost, req.URL.Host) -} - -// test for AuthDirector -func TestAuthDirector(t *testing.T) { - h := func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "1", r.Header.Get("X-Torima-UserID")) - fmt.Fprintln(w, "Hello, client") - } - - testDirector := func(proxy *TorimaProxy, req *http.Request, context *gin.Context) (bool, error) { - session := sessions.Default(context) - - user := ninsho.LINE_USER{ - Sub: "1", - } - json, _ := json.Marshal(user) - - session.Set("user", string(json)) - err := session.Save() - assert.NoError(t, err) - - c, err := AuthDirector(proxy, req, context) - - assert.NoError(t, err) - assert.Equal(t, CONTINUE, c) - - return CONTINUE, nil - } - - DEFAULT_DIRECTORS = []TorimaDirector{ - testDirector, - } - - req, recorder, _, proxy := directorSample(t) - mockServer, _ := setupMockServer(h, req, t) - defer mockServer.Close() - - req.URL.Path = "/hello?hoge" - - proxy.Engine.ServeHTTP(recorder, req) - assert.Equal(t, http.StatusOK, recorder.Result().StatusCode) -} - -func TestAuthDirectorWithWhiteList(t *testing.T) { - h := func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "Hello, client") - } - - DEFAULT_DIRECTORS = []TorimaDirector{ - AuthDirector, - } - - req, recorder, _, proxy := directorSample(t) - mockServer, _ := setupMockServer(h, req, t) - defer mockServer.Close() - - proxy.Config.WhiteListPath = []string{ - "/hello", - } - - proxy.Engine.ServeHTTP(recorder, req) - assert.Equal(t, http.StatusOK, recorder.Result().StatusCode) -} - -// test for AuthDirector -func TestAuthDirectorNoPermit(t *testing.T) { - DEFAULT_DIRECTORS = []TorimaDirector{ - AuthDirector, - } - - req, recorder, _, proxy := directorSample(t) - req.URL.Path = "/hello" - - proxy.Engine.ServeHTTP(recorder, req) - assert.Equal(t, http.StatusUnauthorized, recorder.Result().StatusCode) -} - -type TestResponseRecorder struct { - *httptest.ResponseRecorder - closeChannel chan bool -} - -func (r *TestResponseRecorder) CloseNotify() <-chan bool { - return r.closeChannel -} - -func (r *TestResponseRecorder) closeClient() { - r.closeChannel <- true -} - -func CreateTestResponseRecorder() *TestResponseRecorder { - return &TestResponseRecorder{ - httptest.NewRecorder(), - make(chan bool, 1), - } -} - -func TestLogDirector(t *testing.T) { - req, recorder, context, proxy := directorSample(t) - - before, err := proxy.Database.Client.RequestLog.Query().Count(proxy.Database.Ctx) - assert.NoError(t, err) - - req.URL.Path = "/" - - BeforeLogDirector(proxy, req, context) - - assert.Equal(t, http.StatusOK, recorder.Result().StatusCode) - - after, err := proxy.Database.Client.RequestLog.Query().Count(proxy.Database.Ctx) - assert.NoError(t, err) - - assert.Equal(t, before+1, after) - - all, err := proxy.Database.Client.RequestLog.Query().All(proxy.Database.Ctx) - assert.NoError(t, err) - - requestLog := all[after-1] - t.Log("--- HEADER ---") - t.Log(requestLog.Headers) - - assert.Equal(t, "before", requestLog.Flag) -} diff --git a/core/errors.go b/core/errors.go deleted file mode 100644 index 3a6241b..0000000 --- a/core/errors.go +++ /dev/null @@ -1,61 +0,0 @@ -package core - -import ( - "fmt" - "net/http" - "strings" - - "github.com/gin-gonic/gin" -) - -var unauthorizedErrorTag = "failed to authorize users" -var failedToSplitErrorTag = "failed to split error tag" - -var errorStatusMap = map[string]int{ - unauthorizedErrorTag: http.StatusUnauthorized, -} - -func makeError(e error, tag string) error { - if e == nil { - return nil - } - - return fmt.Errorf("%s: %v", tag, e) -} - -func splitErrorTag(err error) (string, error) { - errMsg := err.Error() - - splited := strings.Split(errMsg, ":") - if len(splited) < 1 { - return "", makeError(err, failedToSplitErrorTag) - } - - return splited[0], nil -} - -func findStatusCodeByErr(err *error) int { - var statusCode = http.StatusInternalServerError - - tag, splitErr := splitErrorTag(*err) - if splitErr != nil { - return statusCode - } - - if val, ok := errorStatusMap[tag]; ok { - statusCode = val - } - - return statusCode -} - -func abordGin(proxy *TorimaProxy, err error, c *gin.Context) { - statusCode := findStatusCodeByErr(&err) - tag, _ := splitErrorTag(err) - fmt.Printf("error: %d, %v, %v", statusCode, err, tag) - - c.Status(statusCode) - c.Writer.WriteString(scripts) - c.Writer.WriteString(backHTML) - c.Abort() -} diff --git a/core/log.go b/core/log.go deleted file mode 100644 index 604c3a1..0000000 --- a/core/log.go +++ /dev/null @@ -1,55 +0,0 @@ -package core - -import ( - "fmt" - "log" - "net/http" - "reflect" - "runtime" -) - -type FlowLog struct { - name string - result bool -} - -type FlowLogger struct { - logs []FlowLog -} - -func NewFlowLogger() FlowLogger { - return FlowLogger{ - logs: []FlowLog{}, - } -} - -func (logger *FlowLogger) Add(f any, result bool) { - rv := reflect.ValueOf(f) - ptr := rv.Pointer() - name := runtime.FuncForPC(ptr).Name() - - newLog := FlowLog{ - name, - result, - } - - logger.logs = append(logger.logs, newLog) -} - -func (flowLogs *FlowLogger) Show() { - log.Println("\n--- start ----") - - for _, v := range flowLogs.logs { - log.Printf("name: %v\n", v.name) - log.Printf("result: %v\n", v.result) - } - - fmt.Println("--- end ----") -} - -/** - * LogReq is the function that logs the request. -**/ -func LogReq(req *http.Request) { - fmt.Printf("[%s] %s%s\n=> %s%s\n\n", req.Method, req.Host, req.RequestURI, req.URL.Host, req.URL.Path) -} diff --git a/core/modify_resp.go b/core/modify_resp.go deleted file mode 100644 index 2aa0f80..0000000 --- a/core/modify_resp.go +++ /dev/null @@ -1,53 +0,0 @@ -package core - -import ( - "bytes" - "fmt" - "io/ioutil" - "log" - "net/http" - "strconv" - - "github.com/PuerkitoBio/goquery" - "github.com/gin-gonic/gin" -) - -func MainModifyResponse(proxy *TorimaProxy, res *http.Response) { - fmt.Printf("=> %v\n", res.Request.URL) -} - -func InjectHTMLModifyResponse(html string, proxy *TorimaProxy, res *http.Response, c *gin.Context) (bool, error) { - document, err := goquery.NewDocumentFromReader(res.Body) - if err != nil { - log.Fatal(err) - } - - document.Find("body").AppendHtml(html) - - html, err = document.Html() - if err != nil { - return FINISHED, err - } - - // fmt.Printf("%v", html) - - b := []byte(html) - res.Body = ioutil.NopCloser(bytes.NewReader(b)) - - res.Header.Set("Content-Length", strconv.Itoa(len(b))) - res.ContentLength = int64(len(b)) - - return CONTINUE, nil -} - -func InjectServiceWorkerModifyResponse(proxy *TorimaProxy, res *http.Response, c *gin.Context) (bool, error) { - contentType := res.Header.Get("Content-Type") - - if contentType != "text/html; charset=utf-8" { - return CONTINUE, nil - } - - html := scripts + "\n" - - return InjectHTMLModifyResponse(html, proxy, res, c) -} diff --git a/core/param.go b/core/param.go deleted file mode 100644 index 84ceae1..0000000 --- a/core/param.go +++ /dev/null @@ -1,42 +0,0 @@ -package core - -import ( - gin_ninsho "github.com/ochanoco/ninsho/extension/gin" -) - -/* configuration of DB */ -var DB_TYPE = readEnv("TORIMA_DB_TYPE", "sqlite3") -var DB_CONFIG = readEnv("TORIMA_DB_CONFIG", "file:./data/db.sqlite3?_fk=1") -var SECRET = readEnv("TORIMA_SECRET", randomString(32)) - -/* other */ -var DEFAULT_DIRECTORS = []TorimaDirector{ - BeforeLogDirector, - SanitizeHeaderDirector, - AuthDirector, - DefaultRouteDirector, - ThirdPartyDirector, - AfterLogDirector, -} - -var DEFAULT_MODIFY_RESPONSES = []TorimaModifyResponse{ - InjectServiceWorkerModifyResponse, -} - -var DEFAULT_PROXYWEB_PAGES = []TorimaProxyWebPage{ - ConfigWeb, - StaticWeb, - LoginWebs, -} - -var CONFIG_FILE = "./config.yaml" -var STATIC_FOLDER = "./static" - -var AUTH_PATH = gin_ninsho.NinshoGinPath{ - Unauthorized: "/auth/login", - Callback: "/auth/callback", - AfterAuth: "/_torima/back", -} - -var CLIENT_ID = readEnvOrPanic("TORIMA_CLIENT_ID") -var CLIENT_SECRET = readEnvOrPanic("TORIMA_CLIENT_SECRET") diff --git a/core/params.go b/core/params.go new file mode 100644 index 0000000..b9e07f9 --- /dev/null +++ b/core/params.go @@ -0,0 +1,14 @@ +package core + +import "github.com/ochanoco/torima/utils" + +/* configuration of DB */ +var DB_TYPE = utils.ReadEnv("TORIMA_DB_TYPE", "sqlite3") +var DB_CONFIG = utils.ReadEnv("TORIMA_DB_CONFIG", "file:./data/db.sqlite3?_fk=1") +var SECRET = utils.ReadEnv("TORIMA_SECRET", utils.RandomString(32)) + +var CONFIG_FILE = "./config.yaml" +var STATIC_FOLDER = "./static" + +var CLIENT_ID = utils.ReadEnvOrPanic("TORIMA_CLIENT_ID") +var CLIENT_SECRET = utils.ReadEnvOrPanic("TORIMA_CLIENT_SECRET") diff --git a/core/proxy.go b/core/proxy.go index ccf5e02..2e3c6ae 100644 --- a/core/proxy.go +++ b/core/proxy.go @@ -4,50 +4,57 @@ import ( "net/http" "github.com/gin-gonic/gin" + "github.com/ochanoco/torima/utils" ) -const CONTINUE = true -const FINISHED = false - -type TorimaPackageArgument interface{ *http.Request | *http.Response } - -func runAllPackage[T TorimaPackageArgument]( - pkgs []func(*TorimaProxy, T, *gin.Context) (bool, error), - args T, proxy *TorimaProxy, c *gin.Context) { - - logger := NewFlowLogger() +func runAllExtension[T TorimaPackageTarget]( + pkgs []func(*TorimaPackageContext[T]) (int, error), + c *TorimaPackageContext[T]) { for _, pkg := range pkgs { - isContinuing, err := pkg(proxy, args, c) - logger.Add(pkg, isContinuing) + status, err := pkg(c) + + if status != Keep { + c.PackageStatus = status + } if err != nil { - abordGin(proxy, err, c) + utils.AbordGin(err, c.GinContext) } - if !isContinuing { + if status == ForceStop { break } } - - logger.Show() } /** * Directors is a list of functions that modify the * request before it is sent to the target server. **/ -func (proxy *TorimaProxy) Director(req *http.Request, c *gin.Context) { - runAllPackage(proxy.Directors, req, proxy, c) +func (proxy *TorimaProxy) Director(req *http.Request, ginContext *gin.Context) { + c := TorimaDirectorPackageContext{ + Proxy: proxy, + Target: req, + GinContext: ginContext, + PackageStatus: AuthNeeded, + } - LogReq(req) + runAllExtension[*http.Request](proxy.Directors, &c) } /** * ModifyResponses is a list of functions that modify the * response before it is sent to the client. **/ -func (proxy *TorimaProxy) ModifyResponse(res *http.Response, c *gin.Context) error { - runAllPackage(proxy.ModifyResponses, res, proxy, c) +func (proxy *TorimaProxy) ModifyResponse(res *http.Response, ginContext *gin.Context) error { + c := TorimaModifyResponsePackageContext{ + Proxy: proxy, + Target: res, + GinContext: ginContext, + PackageStatus: Keep, + } + + runAllExtension(proxy.ModifyResponses, &c) return nil } diff --git a/docker/Dockerfile b/docker/Dockerfile index fbd5fe7..6c3727b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -20,10 +20,15 @@ RUN nix-shell /docker/default.nix --command "go mod download" # build serv COPY ./core /workspace/core -COPY ./serv /workspace/serv +COPY ./proxy /workspace/proxy +COPY ./extension /workspace/extension +COPY ./utils /workspace/utils COPY ./ent /workspace/ent -COPY main.go /workspace +COPY ./serv /workspace/serv + +WORKDIR /workspace/serv RUN nix-shell /docker/default.nix --command "go build" +WORKDIR /workspace COPY ./static /workspace/static -CMD ["/workspace/torima"] \ No newline at end of file +CMD ["/workspace/serv/serv"] \ No newline at end of file diff --git a/extension/directors/auth.go b/extension/directors/auth.go new file mode 100644 index 0000000..65823b0 --- /dev/null +++ b/extension/directors/auth.go @@ -0,0 +1,49 @@ +package directors + +import ( + "fmt" + + "github.com/ochanoco/ninsho" + gin_ninsho "github.com/ochanoco/ninsho/extension/gin" + "github.com/ochanoco/torima/core" + "github.com/ochanoco/torima/utils" + + "golang.org/x/exp/slices" +) + +func SkipAuthDirector(c *core.TorimaDirectorPackageContext) (core.TorimaPackageStatus, error) { + if c.Target.Method == "GET" && c.Target.URL.RawQuery == "" { + if c.Target.URL.Path == "/" { + return core.NoAuthNeeded, nil + } + + if slices.Contains(c.Proxy.Config.SkipAuthList, c.Target.URL.Path) { + return core.NoAuthNeeded, nil + } + } + + return core.AuthNeeded, nil +} + +func AuthDirector(c *core.TorimaDirectorPackageContext) (core.TorimaPackageStatus, error) { + if c.PackageStatus == core.NoAuthNeeded { + return core.NoAuthNeeded, nil + } + + user, err := gin_ninsho.LoadUser[ninsho.LINE_USER](c.GinContext) + + // just to be sure + c.Target.Header.Del("X-Torima-UserID") + + if err != nil { + err = utils.MakeError(err, "failed to get user from session: ") + return core.ForceStop, err + } + + if user != nil { + c.Target.Header.Set("X-Torima-UserID", user.Sub) + return core.Authed, nil + } + + return core.ForceStop, utils.MakeError(fmt.Errorf(""), utils.UnauthorizedErrorTag) +} diff --git a/extension/directors/log.go b/extension/directors/log.go new file mode 100644 index 0000000..c685821 --- /dev/null +++ b/extension/directors/log.go @@ -0,0 +1,40 @@ +package directors + +import ( + "bytes" + "net/http/httputil" + + "github.com/ochanoco/torima/core" + "github.com/ochanoco/torima/utils" +) + +func MakeLogDirector(flag string) core.TorimaDirector { + return func(c *core.TorimaDirectorPackageContext) (core.TorimaPackageStatus, error) { + request, err := httputil.DumpRequest(c.Target, true) + + if err != nil { + err = utils.MakeError(err, "failed to dump headers to json: ") + return core.ForceStop, err + } + + splited := bytes.Split(request, []byte("\r\n\r\n")) + + header := splited[0] + headerLen := len(header) + + body := request[headerLen:] + + l := c.Proxy.Database.CreateRequestLog(string(header), body, flag) + _, err = l.Save(c.Proxy.Database.Ctx) + + if err != nil { + err = utils.MakeError(err, "failed to save request: ") + return core.ForceStop, err + } + + return core.Keep, err + } +} + +var BeforeLogDirector = MakeLogDirector("before") +var AfterLogDirector = MakeLogDirector("after") diff --git a/extension/directors/route.go b/extension/directors/route.go new file mode 100644 index 0000000..cd6026f --- /dev/null +++ b/extension/directors/route.go @@ -0,0 +1,35 @@ +package directors + +import ( + "fmt" + "strings" + + "github.com/ochanoco/torima/core" +) + +func BasicRoute(host string, c *core.TorimaDirectorPackageContext) (core.TorimaPackageStatus, error) { + c.Target.URL.Host = host + + // just to be sure + c.Target.Header.Del("X-Torima-Proxy-Token") + c.Target.Header.Set("X-Torima-Proxy-Token", core.SECRET) + + c.Target.URL.Scheme = c.Proxy.Config.Scheme + + return core.Keep, nil +} + +func DefaultRouteDirector(c *core.TorimaDirectorPackageContext) (core.TorimaPackageStatus, error) { + if strings.HasPrefix(c.Target.URL.Path, "/torima/") { + return core.Keep, nil + } + + host := c.Proxy.Config.DefaultOrigin + + if host == "" { + err := fmt.Errorf("failed to get destination config (%s)", host) + return core.ForceStop, err + } + + return BasicRoute(host, c) +} diff --git a/extension/directors/utils.go b/extension/directors/utils.go new file mode 100644 index 0000000..db55442 --- /dev/null +++ b/extension/directors/utils.go @@ -0,0 +1,55 @@ +package directors + +import ( + "net/http" + "strings" + + "github.com/ochanoco/torima/core" +) + +func ThirdPartyDirector(c *core.TorimaDirectorPackageContext) (core.TorimaPackageStatus, error) { + path := strings.Split(c.Target.URL.Path, "/") + hasRedirectPrefix := strings.HasPrefix(c.Target.URL.Path, "/torima/redirect/") + + if !hasRedirectPrefix || len(path) < 3 { + return core.Keep, nil + } + + for _, origin := range c.Proxy.Config.ProtectionScope { + if origin == path[3] { + c.Target.Host = origin + c.Target.URL.Host = origin + + p := strings.Join(path[4:], "/") + c.Target.URL.Path = "/" + p + + c.Target.URL.Scheme = "https" + return BasicRoute(origin, c) + } + } + + return core.Keep, nil +} + +func SanitizeHeaderDirector(c *core.TorimaDirectorPackageContext) (core.TorimaPackageStatus, error) { + headers := http.Header{ + "Host": {c.Proxy.Config.Host}, + "User-Agent": {"torima"}, + + "Content-Type": c.Target.Header["Content-Type"], + "Content-Length": c.Target.Header["Content-Length"], + + "Accept": c.Target.Header["Accept"], + "Connection": c.Target.Header["Connection"], + + "Accept-Encoding": c.Target.Header["Accept-Encoding"], + "Accept-Language": c.Target.Header["Accept-Language"], + + "Cookie": c.Target.Header["Cookie"], + } + + c.Target.Header = headers + + return core.Keep, nil + +} diff --git a/extension/modify_resp.go b/extension/modify_resp.go new file mode 100644 index 0000000..d2feb7d --- /dev/null +++ b/extension/modify_resp.go @@ -0,0 +1,54 @@ +package extension + +import ( + "bytes" + "fmt" + "io" + "log" + "net/http" + "strconv" + + "github.com/PuerkitoBio/goquery" + "github.com/ochanoco/torima/core" + "github.com/ochanoco/torima/utils" +) + +func MainModifyResponse(proxy *core.TorimaProxy, res *http.Response) { + fmt.Printf("=> %v\n", res.Request.URL) +} + +func InjectHTML(html string, c *core.TorimaModifyResponsePackageContext) (core.TorimaPackageStatus, error) { + document, err := goquery.NewDocumentFromReader(c.Target.Body) + if err != nil { + log.Fatal(err) + } + + document.Find("body").AppendHtml(html) + + html, err = document.Html() + if err != nil { + return core.ForceStop, err + } + + // fmt.Printf("%v", html) + + b := []byte(html) + c.Target.Body = io.NopCloser(bytes.NewReader(b)) + + c.Target.Header.Set("Content-Length", strconv.Itoa(len(b))) + c.Target.ContentLength = int64(len(b)) + + return core.Keep, nil +} + +func InjectServiceWorkerModifyResponse(c *core.TorimaModifyResponsePackageContext) (core.TorimaPackageStatus, error) { + contentType := c.Target.Header.Get("Content-Type") + + if contentType != "text/html; charset=utf-8" { + return core.Keep, nil + } + + html := utils.Scripts + "\n" + + return InjectHTML(html, c) +} diff --git a/extension/param.go b/extension/param.go new file mode 100644 index 0000000..9c0ac7c --- /dev/null +++ b/extension/param.go @@ -0,0 +1,11 @@ +package extension + +import ( + gin_ninsho "github.com/ochanoco/ninsho/extension/gin" +) + +var AUTH_PATH = gin_ninsho.NinshoGinPath{ + Unauthorized: "/auth/login", + Callback: "/auth/callback", + AfterAuth: "/_torima/back", +} diff --git a/core/web.go b/extension/web.go similarity index 78% rename from core/web.go rename to extension/web.go index 98ce020..ad699b5 100644 --- a/core/web.go +++ b/extension/web.go @@ -1,4 +1,4 @@ -package core +package extension import ( "fmt" @@ -7,39 +7,40 @@ import ( "github.com/gin-gonic/gin" "github.com/ochanoco/ninsho" gin_ninsho "github.com/ochanoco/ninsho/extension/gin" + "github.com/ochanoco/torima/core" ) -func StaticWeb(proxy *TorimaProxy, r *gin.RouterGroup) { +func StaticWeb(proxy *core.TorimaProxy, r *gin.RouterGroup) { r.Use(func() gin.HandlerFunc { return func(c *gin.Context) { c.Writer.Header().Set("Service-Worker-Allowed", "/") } }()) - r.Static("/static", STATIC_FOLDER) + r.Static("/static", core.STATIC_FOLDER) } -func ConfigWeb(proxy *TorimaProxy, r *gin.RouterGroup) { +func ConfigWeb(proxy *core.TorimaProxy, r *gin.RouterGroup) { r.GET("/status", func(c *gin.Context) { session := sessions.Default(c) userId := session.Get("userId") c.JSON(200, gin.H{ "protection_scope": proxy.Config.ProtectionScope, - "white_list_path": proxy.Config.WhiteListPath, + "skip_auth_list": proxy.Config.SkipAuthList, "is_authenticated": userId != nil, // is it needed?. }) }) } -func LoginWebs(proxy *TorimaProxy, r *gin.RouterGroup) { +func LoginWebs(proxy *core.TorimaProxy, r *gin.RouterGroup) { var redirectUri = proxy.Config.Host + proxy.Config.WebRoot + AUTH_PATH.Callback fmt.Printf("please set '%v' to redirect uri\n", redirectUri) var provider = ninsho.Provider{ - ClientID: CLIENT_ID, - ClientSecret: CLIENT_SECRET, + ClientID: core.CLIENT_ID, + ClientSecret: core.CLIENT_SECRET, RedirectUri: redirectUri, Scope: "profile openid", UsePKCE: true, diff --git a/main.go b/main.go index 2557b44..bae2029 100644 --- a/main.go +++ b/main.go @@ -1,9 +1 @@ -package main - -import ( - "github.com/ochanoco/torima/serv" -) - -func main() { - serv.Main() -} +package torima diff --git a/core/serv.go b/proxy/main.go similarity index 59% rename from core/serv.go rename to proxy/main.go index ec17e1f..f6afb1c 100644 --- a/core/serv.go +++ b/proxy/main.go @@ -1,4 +1,4 @@ -package core +package proxy import ( "fmt" @@ -7,21 +7,23 @@ import ( "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/cookie" "github.com/gin-gonic/gin" + "github.com/ochanoco/torima/core" + "github.com/ochanoco/torima/utils" ) -func ProxyServer() (*TorimaProxy, error) { - secret := randomString(64) +func ProxyServer() (*core.TorimaProxy, error) { + secret := utils.RandomString(64) r := gin.Default() store := cookie.NewStore([]byte(secret)) r.Use(sessions.Sessions("torima-session", store)) - db, err := InitDB(DB_CONFIG) + db, err := core.InitDB(core.DB_CONFIG) if err != nil { log.Fatalf("failed to init db: %v", err) } - config, err := readConfig() + config, err := core.ReadConfig() if config == nil { panic("failed to read config: " + err.Error()) } @@ -32,7 +34,7 @@ func ProxyServer() (*TorimaProxy, error) { printConfig(config) - proxy := NewOchancoProxy(r, DEFAULT_DIRECTORS, DEFAULT_MODIFY_RESPONSES, DEFAULT_PROXYWEB_PAGES, config, db) + proxy := core.NewOchancoProxy(r, DEFAULT_DIRECTORS, DEFAULT_MODIFY_RESPONSES, DEFAULT_PROXYWEB_PAGES, config, db) return &proxy, nil } diff --git a/proxy/param.go b/proxy/param.go new file mode 100644 index 0000000..253f206 --- /dev/null +++ b/proxy/param.go @@ -0,0 +1,28 @@ +package proxy + +import ( + "github.com/ochanoco/torima/core" + "github.com/ochanoco/torima/extension" + "github.com/ochanoco/torima/extension/directors" +) + +/* other */ +var DEFAULT_DIRECTORS = core.TorimaDirectors{ + directors.BeforeLogDirector, + directors.SanitizeHeaderDirector, + directors.SkipAuthDirector, + directors.AuthDirector, + directors.DefaultRouteDirector, + directors.ThirdPartyDirector, + directors.AfterLogDirector, +} + +var DEFAULT_MODIFY_RESPONSES = core.TorimaModifyResponses{ + extension.InjectServiceWorkerModifyResponse, +} + +var DEFAULT_PROXYWEB_PAGES = []core.TorimaProxyWebPage{ + extension.ConfigWeb, + extension.StaticWeb, + extension.LoginWebs, +} diff --git a/proxy/utils.go b/proxy/utils.go new file mode 100644 index 0000000..2bbe1a1 --- /dev/null +++ b/proxy/utils.go @@ -0,0 +1,18 @@ +package proxy + +import ( + "fmt" + + "github.com/ochanoco/torima/core" +) + +func printConfig(config *core.TorimaConfig) { + fmt.Println("default_origin:", config.DefaultOrigin) + fmt.Println("host:", config.Host) + fmt.Println("port:", config.Port) + fmt.Println("scheme:", config.Scheme) + + fmt.Println("skip_auth_list:", config.SkipAuthList) + fmt.Println("protection_scope:", config.ProtectionScope) + fmt.Println("web_root:", config.WebRoot) +} diff --git a/serv/main.go b/serv/main.go index ee97754..431db9b 100644 --- a/serv/main.go +++ b/serv/main.go @@ -1,19 +1,20 @@ -package serv +package main import ( "fmt" "github.com/ochanoco/torima/core" + "github.com/ochanoco/torima/proxy" ) const NAME = "line" func Run() (*core.TorimaProxy, error) { - proxyServ, err := core.ProxyServer() + proxyServ, err := proxy.ProxyServer() return proxyServ, err } -func Main() { +func main() { proxyServ, err := Run() if err != nil { panic(err) diff --git a/core/config_test.go b/test/config_test.go similarity index 71% rename from core/config_test.go rename to test/config_test.go index 9400b6a..af02143 100644 --- a/core/config_test.go +++ b/test/config_test.go @@ -1,16 +1,18 @@ -package core +package test import ( "os" "testing" + "github.com/ochanoco/torima/core" + "github.com/ochanoco/torima/utils" "github.com/stretchr/testify/assert" ) var TEST_CONFIG = ` port: 9000 -white_list_path: +skip_auth_list: - /favicon.ico default_origin: 127.0.0.1:9000 @@ -21,16 +23,16 @@ protection_scope: scheme: http ` -func readTestConfig(t *testing.T) (*TorimaConfig, *os.File, error) { +func readTestConfig(t *testing.T) (*core.TorimaConfig, *os.File, error) { file, err := os.CreateTemp("", "config.yaml") assert.NoError(t, err) - CONFIG_FILE = file.Name() + core.CONFIG_FILE = file.Name() _, err = file.Write([]byte(TEST_CONFIG)) assert.NoError(t, err) - config, err := readConfig() + config, err := core.ReadConfig() return config, file, err } @@ -45,14 +47,14 @@ func TestReadConfig(t *testing.T) { assert.Equal(t, 9000, config.Port) assert.Equal(t, "127.0.0.1:9000", config.DefaultOrigin) assert.Equal(t, "http", config.Scheme) - assert.Equal(t, "/favicon.ico", config.WhiteListPath[0]) + assert.Equal(t, "/favicon.ico", config.SkipAuthList[0]) assert.Equal(t, "example.com", config.ProtectionScope[0]) } func TestReadConfigDefault(t *testing.T) { - CONFIG_FILE = "" + core.CONFIG_FILE = "" - config, err := readConfig() + config, err := core.ReadConfig() assert.Error(t, err) assert.NotNil(t, config) @@ -60,7 +62,7 @@ func TestReadConfigDefault(t *testing.T) { assert.Equal(t, "http://127.0.0.1:8080", config.Host) assert.Equal(t, 8080, config.Port) assert.Equal(t, "http", config.Scheme) - assert.Equal(t, 0, len(config.WhiteListPath)) + assert.Equal(t, 0, len(config.SkipAuthList)) assert.Equal(t, 0, len(config.ProtectionScope)) assert.Equal(t, "/torima", config.WebRoot) } @@ -69,9 +71,9 @@ func TestReadConfigDefault(t *testing.T) { func TestReadEnv(t *testing.T) { os.Setenv("TORIMA_TEST1", "TEST") - env := readEnv("TORIMA_TEST1", "TEST") + env := utils.ReadEnv("TORIMA_TEST1", "TEST") assert.Equal(t, "TEST", env) - env = readEnv("TORIMA_TEST2", "TEST") + env = utils.ReadEnv("TORIMA_TEST2", "TEST") assert.Equal(t, "TEST", env) } diff --git a/test/director_test.go b/test/director_test.go new file mode 100644 index 0000000..19b11ec --- /dev/null +++ b/test/director_test.go @@ -0,0 +1,277 @@ +package test + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "os" + "testing" + + "github.com/gin-contrib/sessions" + "github.com/gin-contrib/sessions/cookie" + "github.com/gin-gonic/gin" + "github.com/ochanoco/ninsho" + "github.com/ochanoco/torima/core" + "github.com/ochanoco/torima/extension/directors" + "github.com/ochanoco/torima/proxy" + "github.com/ochanoco/torima/test/tools" + "github.com/stretchr/testify/assert" +) + +func directorSample(t *testing.T) (*core.TorimaPackageContext[*http.Request], *TestResponseRecorder) { + logger := tools.ExtensionLogger{} + + core.DB_TYPE = "sqlite3" + core.DB_CONFIG = "../data/test.db?_fk=1" + core.SECRET = "test_secret" + + recorder := CreateTestResponseRecorder() + ginContext, r := gin.CreateTestContext(recorder) + + store := cookie.NewStore([]byte("test")) + r.Use(sessions.Sessions("torima-session", store)) + + db, err := core.InitDB(core.DB_CONFIG) + assert.NoError(t, err) + + config, file, err := readTestConfig(t) + assert.NoError(t, err) + defer os.Remove(file.Name()) + + proxy := core.NewOchancoProxy(r, logger.InjectDirectors(proxy.DEFAULT_DIRECTORS), proxy.DEFAULT_MODIFY_RESPONSES, proxy.DEFAULT_PROXYWEB_PAGES, config, db) + req := httptest.NewRequest("GET", "http://localhost:8080/", nil) + + ctx := core.TorimaPackageContext[*http.Request]{ + GinContext: ginContext, + Proxy: &proxy, + Target: req, + } + + return &ctx, recorder +} + +func setupMockServer(handler http.HandlerFunc, req *http.Request, t *testing.T) (*httptest.Server, *url.URL) { + h := http.HandlerFunc(handler) + + ts := httptest.NewServer(h) + u, err := url.Parse(ts.URL) + assert.NoError(t, err) + + req.URL.Path = "/hello" + req.URL.Host = u.Host + req.Host = u.Host + + return ts, u +} + +// test for RouteDirector +func TestRouteDirector(t *testing.T) { + ctx, _ := directorSample(t) + c, err := directors.BasicRoute("example.com", ctx) + + assert.NoError(t, err) + assert.Equal(t, core.Keep, c) + assert.Equal(t, "example.com", ctx.Target.URL.Host) + assert.Equal(t, "http", ctx.Target.URL.Scheme) + assert.Equal(t, core.SECRET, ctx.Target.Header.Get("X-Torima-Proxy-Token")) +} + +// test for DefaultRouteDirector +func TestThirdPartyDirector(t *testing.T) { + h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, "Hello, client") + }) + + ts := httptest.NewServer(h) + defer ts.Close() + + u, err := url.Parse(ts.URL) + assert.NoError(t, err) + + host := fmt.Sprintf("%v:%v", u.Host, u.Port()) + + ctx, _ := directorSample(t) + + ctx.Target.URL.Path = "/torima/redirect/" + host + + ctx.Proxy.Config.ProtectionScope = []string{host} + + c, err := directors.ThirdPartyDirector(ctx) + assert.NoError(t, err) + assert.Equal(t, core.Keep, c) + + c, err = directors.ThirdPartyDirector(ctx) + assert.NoError(t, err) + + assert.Equal(t, core.Keep, c) + assert.Equal(t, host, ctx.Target.URL.Host) +} + +// test for DefaultRouteDirector +func TestThirdPartyDirectorNoParmit(t *testing.T) { + unpermitHost := "not-in-list.example.com" + + ctx, _ := directorSample(t) + + ctx.Target.URL.Path = "/torima/redirect/" + unpermitHost + "/" + + c, err := directors.ThirdPartyDirector(ctx) + assert.NoError(t, err) + assert.Equal(t, core.Keep, c) + + c, err = directors.ThirdPartyDirector(ctx) + assert.NoError(t, err) + + assert.Equal(t, core.Keep, c) + assert.NotEqual(t, unpermitHost, ctx.Target.URL.Host) +} + +// test for AuthDirector +func TestAuthDirector(t *testing.T) { + h := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "1", r.Header.Get("X-Torima-UserID")) + fmt.Fprintln(w, "Hello, client") + } + + testDirector := func(c *core.TorimaPackageContext[*http.Request]) (core.TorimaPackageStatus, error) { + session := sessions.Default(c.GinContext) + + user := ninsho.LINE_USER{ + Sub: "1", + } + json, _ := json.Marshal(user) + + session.Set("user", string(json)) + err := session.Save() + assert.NoError(t, err) + + status, err := directors.AuthDirector(c) + + assert.NoError(t, err) + assert.Equal(t, core.Authed, status) + + return core.Authed, nil + } + + proxy.DEFAULT_DIRECTORS = core.TorimaDirectors{ + testDirector, + } + + ctx, recorder := directorSample(t) + mockServer, _ := setupMockServer(h, ctx.Target, t) + defer mockServer.Close() + + ctx.Target.URL.Path = "/hello?hoge" + + ctx.Proxy.Engine.ServeHTTP(recorder, ctx.Target) + assert.Equal(t, http.StatusOK, recorder.Result().StatusCode) +} + +func TestSkipAuthList(t *testing.T) { + h := func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, "Hello, client") + } + + proxy.DEFAULT_DIRECTORS = core.TorimaDirectors{ + directors.SkipAuthDirector, + directors.AuthDirector, + } + + ctx, recorder := directorSample(t) + mockServer, _ := setupMockServer(h, ctx.Target, t) + defer mockServer.Close() + + ctx.Proxy.Config.SkipAuthList = []string{ + "/hello", + } + + ctx.Target.URL.Path = "/hello" + ctx.Proxy.Engine.ServeHTTP(recorder, ctx.Target) + assert.Equal(t, http.StatusOK, recorder.Result().StatusCode) +} + +func TestForceAuthList(t *testing.T) { + h := func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, "Hello, client") + } + + proxy.DEFAULT_DIRECTORS = core.TorimaDirectors{ + directors.DefaultRouteDirector, + directors.ForceAuthDirector, + directors.AuthDirector, + } + + ctx, recorder := directorSample(t) + mockServer, _ := setupMockServer(h, ctx.Target, t) + defer mockServer.Close() + + ctx.Proxy.Config.ForceAuthList = []string{ + "/", + } + + ctx.Target.URL.Path = "/" + ctx.Proxy.Engine.ServeHTTP(recorder, ctx.Target) + assert.Equal(t, http.StatusUnauthorized, recorder.Result().StatusCode) +} + +// test for AuthDirector +func TestAuthDirectorNoPermit(t *testing.T) { + proxy.DEFAULT_DIRECTORS = core.TorimaDirectors{ + directors.AuthDirector, + } + + ctx, recorder := directorSample(t) + ctx.Target.URL.Path = "/hello" + + ctx.Proxy.Engine.ServeHTTP(recorder, ctx.Target) + assert.Equal(t, http.StatusUnauthorized, recorder.Result().StatusCode) +} + +type TestResponseRecorder struct { + *httptest.ResponseRecorder + closeChannel chan bool +} + +func (r *TestResponseRecorder) CloseNotify() <-chan bool { + return r.closeChannel +} + +func (r *TestResponseRecorder) closeClient() { + r.closeChannel <- true +} + +func CreateTestResponseRecorder() *TestResponseRecorder { + return &TestResponseRecorder{ + httptest.NewRecorder(), + make(chan bool, 1), + } +} + +func TestLogDirector(t *testing.T) { + ctx, recorder := directorSample(t) + + before, err := ctx.Proxy.Database.Client.RequestLog.Query().Count(ctx.Proxy.Database.Ctx) + assert.NoError(t, err) + + ctx.Target.URL.Path = "/" + + directors.BeforeLogDirector(ctx) + + assert.Equal(t, http.StatusOK, recorder.Result().StatusCode) + + after, err := ctx.Proxy.Database.Client.RequestLog.Query().Count(ctx.Proxy.Database.Ctx) + assert.NoError(t, err) + + assert.Equal(t, before+1, after) + + all, err := ctx.Proxy.Database.Client.RequestLog.Query().All(ctx.Proxy.Database.Ctx) + assert.NoError(t, err) + + requestLog := all[after-1] + t.Log("--- HEADER ---") + t.Log(requestLog.Headers) + + assert.Equal(t, "before", requestLog.Flag) +} diff --git a/core/errors_test.go b/test/errors_test.go similarity index 61% rename from core/errors_test.go rename to test/errors_test.go index 31ddd84..f63fff4 100644 --- a/core/errors_test.go +++ b/test/errors_test.go @@ -1,16 +1,17 @@ -package core +package test import ( "errors" "testing" + "github.com/ochanoco/torima/utils" "github.com/stretchr/testify/assert" ) // test for splitErrorTagfunc TestSplitErrorTag() { func TestSplitErrorTag(t *testing.T) { err := errors.New("test error: this is test error") - tag, err := splitErrorTag(err) + tag, err := utils.SplitErrorTag(err) assert.NoError(t, err) assert.Equal(t, "test error", tag) @@ -19,11 +20,11 @@ func TestSplitErrorTag(t *testing.T) { // test for findStatusCodeByErr func TestFindStatusCodeByErr(t *testing.T) { err := errors.New("") - unauthorizedErr := makeError(err, unauthorizedErrorTag) - unexpectedErr := makeError(err, "unexpected error") + unauthorizedErr := utils.MakeError(err, utils.UnauthorizedErrorTag) + unexpectedErr := utils.MakeError(err, "unexpected error") - unauthorizedErrStatusCode := findStatusCodeByErr(&unauthorizedErr) - unexpectedError := findStatusCodeByErr(&unexpectedErr) + unauthorizedErrStatusCode := utils.FindStatusCodeByErr(&unauthorizedErr) + unexpectedError := utils.FindStatusCodeByErr(&unexpectedErr) assert.Equal(t, 401, unauthorizedErrStatusCode) assert.Equal(t, 500, unexpectedError) diff --git a/test/log_test.go b/test/log_test.go new file mode 100644 index 0000000..0b56204 --- /dev/null +++ b/test/log_test.go @@ -0,0 +1,36 @@ +package test + +import ( + "fmt" + "testing" + + "github.com/ochanoco/torima/core" + "github.com/ochanoco/torima/extension/directors" + "github.com/ochanoco/torima/test/tools" +) + +func TestExtensionLog(t *testing.T) { + core.DB_TYPE = "sqlite3" + core.DB_CONFIG = "../data/test.db?_fk=1" + core.SECRET = "test_secret" + + logger := tools.ExtensionLogger{} + + directors := core.TorimaDirectors{ + directors.DefaultRouteDirector, + directors.DefaultRouteDirector, + } + + directors = logger.InjectDirectors(directors) + + if len(directors) != 5 { + t.Errorf("InjectDirector failed") + } + + fmt.Printf("Directors: %v\n", directors) + + c, _ := directorSample(t) + c.Proxy.Directors = directors + + c.Proxy.Director(c.Target, c.GinContext) +} diff --git a/test/tools/log.go b/test/tools/log.go new file mode 100644 index 0000000..7179063 --- /dev/null +++ b/test/tools/log.go @@ -0,0 +1,76 @@ +package tools + +import ( + "log" + "reflect" + "runtime" + + "github.com/ochanoco/torima/core" +) + +var STATE = map[int]string{ + 0: "AuthNeeded", + 1: "Authed", + 2: "NoAuthNeeded", + 3: "ForceStop", + 4: "Keep", +} + +type ExtensionLogger struct { +} + +func (logger *ExtensionLogger) Director(count int) core.TorimaDirector { + return func(c *core.TorimaDirectorPackageContext) (int, error) { + Log(count, c.Proxy.Directors[count*2+2], c.PackageStatus, c.Target.URL.Path) + return core.Keep, nil + } +} + +func (logger *ExtensionLogger) ModifyResp(count int) core.TorimaModifyResponse { + return func(c *core.TorimaModifyResponsePackageContext) (int, error) { + Log(count, c.Proxy.Directors[count*2+2], c.PackageStatus, "") + return core.Keep, nil + } +} + +func StartOrEndDirector[T *core.TorimaDirectorPackageContext | *core.TorimaModifyResponsePackageContext](c T) (int, error) { + println("---------------------") + return core.Keep, nil +} + +func (logger *ExtensionLogger) InjectDirectors(source core.TorimaDirectors) core.TorimaDirectors { + result := core.TorimaDirectors{StartOrEndDirector[*core.TorimaDirectorPackageContext]} + + for i, v := range source { + d := logger.Director(i) + result = append(result, d) + result = append(result, v) + } + + return result +} + +func (logger *ExtensionLogger) InjectModifyResps(source core.TorimaModifyResponses) core.TorimaModifyResponses { + result := core.TorimaModifyResponses{StartOrEndDirector[*core.TorimaModifyResponsePackageContext]} + + for i, v := range source { + d := logger.ModifyResp(i) + result = append(result, d) + result = append(result, v) + } + + return result +} + +func Log(count int, extension any, result int, path string) { + rv1 := reflect.ValueOf(extension) + ptr1 := rv1.Pointer() + + extensionName := runtime.FuncForPC(ptr1).Name() + + log.Printf("id: %v\n", count) + log.Printf("name: %v\n", extensionName) + log.Printf("result: %v\n", STATE[result]) + log.Printf("path: %v\n", path) + +} diff --git a/utils/env.go b/utils/env.go new file mode 100644 index 0000000..2f63854 --- /dev/null +++ b/utils/env.go @@ -0,0 +1,28 @@ +package utils + +import ( + "fmt" + "log" + "os" +) + +func ReadEnv(name, def string) string { + value := os.Getenv(name) + + if value == "" { + fmt.Printf("environment variable '%v' is not found so that proxy use '%v'\n", name, def) + value = def + } + + return value +} + +func ReadEnvOrPanic(name string) string { + value := os.Getenv(name) + + if value == "" { + log.Fatalf("environment variable '%v' is not found", name) + } + + return value +} diff --git a/utils/errors.go b/utils/errors.go new file mode 100644 index 0000000..b291aa8 --- /dev/null +++ b/utils/errors.go @@ -0,0 +1,61 @@ +package utils + +import ( + "fmt" + "net/http" + "strings" + + "github.com/gin-gonic/gin" +) + +var UnauthorizedErrorTag = "failed to authorize users" +var FailedToSplitErrorTag = "failed to split error tag" + +var errorStatusMap = map[string]int{ + UnauthorizedErrorTag: http.StatusUnauthorized, +} + +func MakeError(e error, tag string) error { + if e == nil { + return nil + } + + return fmt.Errorf("%s: %v", tag, e) +} + +func SplitErrorTag(err error) (string, error) { + errMsg := err.Error() + + splited := strings.Split(errMsg, ":") + if len(splited) < 1 { + return "", MakeError(err, FailedToSplitErrorTag) + } + + return splited[0], nil +} + +func FindStatusCodeByErr(err *error) int { + var statusCode = http.StatusInternalServerError + + tag, splitErr := SplitErrorTag(*err) + if splitErr != nil { + return statusCode + } + + if val, ok := errorStatusMap[tag]; ok { + statusCode = val + } + + return statusCode +} + +func AbordGin(err error, c *gin.Context) { + statusCode := FindStatusCodeByErr(&err) + tag, _ := SplitErrorTag(err) + fmt.Printf("error: %d, %v, %v", statusCode, err, tag) + + c.Status(statusCode) + c.Writer.WriteString(Scripts) + c.Writer.WriteString(BackHTML) + c.Abort() +} diff --git a/core/html.go b/utils/html.go similarity index 87% rename from core/html.go rename to utils/html.go index 2fc5ef3..9fe2e55 100644 --- a/core/html.go +++ b/utils/html.go @@ -1,12 +1,12 @@ -package core +package utils -var scripts = ` +var Scripts = ` ` -var backHTML = ` +var BackHTML = ` ` diff --git a/core/utils.go b/utils/utils.go similarity index 81% rename from core/utils.go rename to utils/utils.go index a20fe79..6308903 100644 --- a/core/utils.go +++ b/utils/utils.go @@ -1,8 +1,8 @@ -package core +package utils import "math/rand" -func randomString(n int) string { +func RandomString(n int) string { var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") s := make([]rune, n)