From 04206b0cb91dccbed331b1b9000b4728eae46a83 Mon Sep 17 00:00:00 2001 From: HAHWUL Date: Mon, 3 Feb 2025 23:07:11 +0900 Subject: [PATCH 1/7] feat: Add functional test fixtures for chi router implementation Signed-off-by: HAHWUL --- spec/functional_test/fixtures/go/chi/go.mod | 5 ++ spec/functional_test/fixtures/go/chi/main.go | 94 ++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 spec/functional_test/fixtures/go/chi/go.mod create mode 100644 spec/functional_test/fixtures/go/chi/main.go diff --git a/spec/functional_test/fixtures/go/chi/go.mod b/spec/functional_test/fixtures/go/chi/go.mod new file mode 100644 index 00000000..e01ef7bf --- /dev/null +++ b/spec/functional_test/fixtures/go/chi/go.mod @@ -0,0 +1,5 @@ +module github.com/hahwul/test-go-app + +go 1.20 + +require github.com/go-chi/chi/v5 v5.2.0 \ No newline at end of file diff --git a/spec/functional_test/fixtures/go/chi/main.go b/spec/functional_test/fixtures/go/chi/main.go new file mode 100644 index 00000000..1b7ec644 --- /dev/null +++ b/spec/functional_test/fixtures/go/chi/main.go @@ -0,0 +1,94 @@ +import ( + //... + "context" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + ) + + func main() { + r := chi.NewRouter() + + // A good base middleware stack + r.Use(middleware.RequestID) + r.Use(middleware.RealIP) + r.Use(middleware.Logger) + r.Use(middleware.Recoverer) + + // Set a timeout value on the request context (ctx), that will signal + // through ctx.Done() that the request has timed out and further + // processing should be stopped. + r.Use(middleware.Timeout(60 * time.Second)) + + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("hi")) + }) + + // RESTy routes for "articles" resource + r.Route("/articles", func(r chi.Router) { + r.With(paginate).Get("/", listArticles) // GET /articles + r.With(paginate).Get("/{month}-{day}-{year}", listArticlesByDate) // GET /articles/01-16-2017 + + r.Post("/", createArticle) // POST /articles + r.Get("/search", searchArticles) // GET /articles/search + + // Regexp url parameters: + r.Get("/{articleSlug:[a-z-]+}", getArticleBySlug) // GET /articles/home-is-toronto + + // Subrouters: + r.Route("/{articleID}", func(r chi.Router) { + r.Use(ArticleCtx) + r.Get("/", getArticle) // GET /articles/123 + r.Put("/", updateArticle) // PUT /articles/123 + r.Delete("/", deleteArticle) // DELETE /articles/123 + }) + }) + + // Mount the admin sub-router + r.Mount("/admin", adminRouter()) + + http.ListenAndServe(":3333", r) + } + + func ArticleCtx(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + articleID := chi.URLParam(r, "articleID") + article, err := dbGetArticle(articleID) + if err != nil { + http.Error(w, http.StatusText(404), 404) + return + } + ctx := context.WithValue(r.Context(), "article", article) + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } + + func getArticle(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + article, ok := ctx.Value("article").(*Article) + if !ok { + http.Error(w, http.StatusText(422), 422) + return + } + w.Write([]byte(fmt.Sprintf("title:%s", article.Title))) + } + + // A completely separate router for administrator routes + func adminRouter() http.Handler { + r := chi.NewRouter() + r.Use(AdminOnly) + r.Get("/", adminIndex) + r.Get("/accounts", adminListAccounts) + return r + } + + func AdminOnly(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + perm, ok := ctx.Value("acl.permission").(YourPermissionType) + if !ok || !perm.IsAdmin() { + http.Error(w, http.StatusText(403), 403) + return + } + next.ServeHTTP(w, r) + }) + } \ No newline at end of file From 2c8d29e8142080ddbb9665a1f0d3d7734f187db7 Mon Sep 17 00:00:00 2001 From: HAHWUL Date: Mon, 3 Feb 2025 23:07:24 +0900 Subject: [PATCH 2/7] feat: Add detector (chi) Signed-off-by: HAHWUL --- spec/unit_test/detector/go/chi_spec.cr | 11 +++++++++++ src/detector/detector.cr | 1 + src/detector/detectors/go/chi.cr | 17 +++++++++++++++++ 3 files changed, 29 insertions(+) create mode 100644 spec/unit_test/detector/go/chi_spec.cr create mode 100644 src/detector/detectors/go/chi.cr diff --git a/spec/unit_test/detector/go/chi_spec.cr b/spec/unit_test/detector/go/chi_spec.cr new file mode 100644 index 00000000..ca48a757 --- /dev/null +++ b/spec/unit_test/detector/go/chi_spec.cr @@ -0,0 +1,11 @@ +require "../../../../src/detector/detectors/go/*" + +describe "Detect Go Chi" do + config_init = ConfigInitializer.new + options = config_init.default_options + instance = Detector::Go::Chi.new options + + it "go.mod" do + instance.detect("go.mod", "github.com/go-chi/chi").should eq(true) + end +end diff --git a/src/detector/detector.cr b/src/detector/detector.cr index 2d38dbbc..ed8f3402 100644 --- a/src/detector/detector.cr +++ b/src/detector/detector.cr @@ -29,6 +29,7 @@ def detect_techs(base_path : String, options : Hash(String, YAML::Any), passive_ Go::Echo, Go::Fiber, Go::Gin, + Go::Chi, Specification::Har, Java::Armeria, Java::Jsp, diff --git a/src/detector/detectors/go/chi.cr b/src/detector/detectors/go/chi.cr new file mode 100644 index 00000000..6147d72a --- /dev/null +++ b/src/detector/detectors/go/chi.cr @@ -0,0 +1,17 @@ +require "../../../models/detector" + +module Detector::Go + class Chi < Detector + def detect(filename : String, file_contents : String) : Bool + if (filename.includes? "go.mod") && (file_contents.includes? "github.com/go-chi/chi") + true + else + false + end + end + + def set_name + @name = "go_chi" + end + end +end From b873f7b466c160810dbe6cf0b6a6356c3422605f Mon Sep 17 00:00:00 2001 From: HAHWUL Date: Mon, 3 Feb 2025 23:19:30 +0900 Subject: [PATCH 3/7] feat: Add Chi router analyzer implementation Signed-off-by: HAHWUL --- src/analyzer/analyzer.cr | 1 + src/analyzer/analyzers/go/chi.cr | 149 +++++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 src/analyzer/analyzers/go/chi.cr diff --git a/src/analyzer/analyzer.cr b/src/analyzer/analyzer.cr index 82e2c65d..086cb56f 100644 --- a/src/analyzer/analyzer.cr +++ b/src/analyzer/analyzer.cr @@ -24,6 +24,7 @@ def initialize_analyzers(logger : NoirLogger) {"go_echo", Go::Echo}, {"go_fiber", Go::Fiber}, {"go_gin", Go::Gin}, + {"go_chi", Go::Chi}, {"har", Specification::Har}, {"java_armeria", Java::Armeria}, {"java_jsp", Java::Jsp}, diff --git a/src/analyzer/analyzers/go/chi.cr b/src/analyzer/analyzers/go/chi.cr new file mode 100644 index 00000000..3355993f --- /dev/null +++ b/src/analyzer/analyzers/go/chi.cr @@ -0,0 +1,149 @@ +require "../../../models/analyzer" +require "../../../minilexers/golang" + +module Analyzer::Go + class Chi < Analyzer + def analyze + result = [] of Endpoint + channel = Channel(String).new + begin + spawn do + Dir.glob("#{@base_path}/**/*") do |file| + channel.send(file) + end + channel.close + end + + WaitGroup.wait do |wg| + @options["concurrency"].to_s.to_i.times do + wg.spawn do + loop do + path = channel.receive? + break if path.nil? + next if File.directory?(path) + if File.exists?(path) && File.extname(path) == ".go" + File.open(path, "r", encoding: "utf-8", invalid: :skip) do |file| + prefix_stack = [] of String + file.each_line.with_index do |line, index| + details = Details.new(PathInfo.new(path, index + 1)) + + # Mount 처리: r.Mount("/prefix", mountedFunc()) + if line.includes?("r.Mount(") + if match = line.match(/r\.Mount\(\s*"([^"]+)"\s*,\s*([^(]+)\(\)/) + mount_prefix = match[1] + router_function = match[2] + # mount된 라우터 함수 내의 엔드포인트를 분석해서 mount prefix를 붙임 + endpoints = analyze_router_function(path, router_function) + endpoints.each do |ep| + ep.url = mount_prefix + ep.url + result << ep + end + end + next + end + + # Route 블록 처리: 블록 내 접두사 저장 + if line.includes?("r.Route(") + if match = line.match(/r\.Route\(\s*"([^"]+)"/) + prefix_stack << match[1] + end + next + end + + # 블록 종료 시 접두사 제거 (단순히 '}'만 있는 줄로 처리) + if line.strip == "}" && !prefix_stack.empty? + prefix_stack.pop + next + end + + # 실제 endpoint 처리: r.Get, r.Post, r.Put, r.Delete + method = "" + if line.includes?("r.Get(") + method = "GET" + elsif line.includes?("r.Post(") + method = "POST" + elsif line.includes?("r.Put(") + method = "PUT" + elsif line.includes?("r.Delete(") + method = "DELETE" + end + + if method.size > 0 + route_path = extract_route_path(line) + if route_path.size > 0 + full_route = (prefix_stack.join("") + route_path).to_s + result << Endpoint.new(full_route, method, details) + end + end + end + end + end + rescue e : File::NotFoundError + logger.debug "File not found: #{path}" + end + end + end + end + rescue e + logger.debug e + end + result + end + + # 기존 extract_route_path: r.Get("/path", ...) 등에서 경로 추출 + def extract_route_path(line : String) : String + if match = line.match(/r\.\w+\(\s*"([^"]+)"/) + return match[1] + end + "" + end + + # 주어진 파일 내에 정의된 router 함수의 내용을 추출하여 엔드포인트 정보로 변환 + def analyze_router_function(file_path : String, func_name : String) : Array(Endpoint) + endpoints = [] of Endpoint + content = File.read(file_path) + # 간단한 정규표현식으로 함수 블록 시작 위치 찾기 + if start_index = content.index("func #{func_name}(") + # 함수 블록의 중괄호 범위를 찾기 위해 플래그 사용 + block_started = false + brace_count = 0 + content.lines.each do |line| + if !block_started + if line.includes?("func #{func_name}(") + block_started = true + if line.includes?("{") + brace_count += 1 + end + end + next + else + brace_count += line.count("{") + brace_count -= line.count("}") + # 함수 내부에서 endpoint 처리 (r.Get, r.Post, ...) + details = Details.new(PathInfo.new(file_path)) + method = "" + if line.includes?("r.Get(") + method = "GET" + elsif line.includes?("r.Post(") + method = "POST" + elsif line.includes?("r.Put(") + method = "PUT" + elsif line.includes?("r.Delete(") + method = "DELETE" + end + + if method.size > 0 + route_path = extract_route_path(line) + if route_path.size > 0 + endpoints << Endpoint.new(route_path, method, details) + end + end + + break if brace_count <= 0 + end + end + end + endpoints + end + end +end \ No newline at end of file From e607ef8abe83261aaa31e19091f752b80a284366 Mon Sep 17 00:00:00 2001 From: HAHWUL Date: Mon, 3 Feb 2025 23:22:01 +0900 Subject: [PATCH 4/7] feat: Add functional tests for Chi router endpoints and improve router function analysis Signed-off-by: HAHWUL --- spec/functional_test/testers/go/chi_spec.cr | 19 +++++++++++++++++++ src/analyzer/analyzers/go/chi.cr | 10 ++++------ 2 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 spec/functional_test/testers/go/chi_spec.cr diff --git a/spec/functional_test/testers/go/chi_spec.cr b/spec/functional_test/testers/go/chi_spec.cr new file mode 100644 index 00000000..7540d137 --- /dev/null +++ b/spec/functional_test/testers/go/chi_spec.cr @@ -0,0 +1,19 @@ +require "../../func_spec.cr" + +expected_endpoints = [ + Endpoint.new("/", "GET"), + Endpoint.new("/articles/", "POST"), + Endpoint.new("/articles/search", "GET"), + Endpoint.new("/articles/{articleSlug:[a-z-]+}", "GET", [Param.new("articleSlug", "", "path")]), + Endpoint.new("/articles/{articleID}/", "GET", [Param.new("articleID", "", "path")]), + Endpoint.new("/articles/{articleID}/", "PUT", [Param.new("articleID", "", "path")]), + Endpoint.new("/articles/{articleID}/", "DELETE", [Param.new("articleID", "", "path")]), + Endpoint.new("/admin/", "GET"), + Endpoint.new("/admin/accounts", "GET"), + Endpoint.new("/accounts", "GET"), +] + +FunctionalTester.new("fixtures/go/chi/", { + :techs => 1, + :endpoints => expected_endpoints.size, +}, expected_endpoints).test_all diff --git a/src/analyzer/analyzers/go/chi.cr b/src/analyzer/analyzers/go/chi.cr index 3355993f..e1947913 100644 --- a/src/analyzer/analyzers/go/chi.cr +++ b/src/analyzer/analyzers/go/chi.cr @@ -102,12 +102,11 @@ module Analyzer::Go def analyze_router_function(file_path : String, func_name : String) : Array(Endpoint) endpoints = [] of Endpoint content = File.read(file_path) - # 간단한 정규표현식으로 함수 블록 시작 위치 찾기 - if start_index = content.index("func #{func_name}(") - # 함수 블록의 중괄호 범위를 찾기 위해 플래그 사용 + # 함수 정의가 포함되어 있는지 확인 + if content.includes?("func #{func_name}(") block_started = false brace_count = 0 - content.lines.each do |line| + content.each_line do |line| if !block_started if line.includes?("func #{func_name}(") block_started = true @@ -119,7 +118,6 @@ module Analyzer::Go else brace_count += line.count("{") brace_count -= line.count("}") - # 함수 내부에서 endpoint 처리 (r.Get, r.Post, ...) details = Details.new(PathInfo.new(file_path)) method = "" if line.includes?("r.Get(") @@ -146,4 +144,4 @@ module Analyzer::Go endpoints end end -end \ No newline at end of file +end From d892a240c0491d3bbfe3d8889e678c6339399f13 Mon Sep 17 00:00:00 2001 From: HAHWUL Date: Mon, 3 Feb 2025 23:27:49 +0900 Subject: [PATCH 5/7] feat: Add support for Chi framework in techs module Signed-off-by: HAHWUL --- src/techs/techs.cr | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/techs/techs.cr b/src/techs/techs.cr index 67adfde0..55031081 100644 --- a/src/techs/techs.cr +++ b/src/techs/techs.cr @@ -142,6 +142,24 @@ module NoirTechs }, }, }, + :go_chi => { + :framework => "Chi", + :language => "Go", + :similar => ["chi", "go-chi", "go_chi"], + :supported => { + :endpoint => true, + :method => true, + :params => { + :query => true, + :path => true, + :body => true, + :header => false, + :cookie => false, + }, + :static_path => false, + :websocket => false, + }, + }, :har => { :format => ["JSON"], :similar => ["har"], From 678cb40ed1bc4b727fab8a9a1d858cfab29b2df8 Mon Sep 17 00:00:00 2001 From: HAHWUL Date: Mon, 3 Feb 2025 23:53:00 +0900 Subject: [PATCH 6/7] feat: Enhance Chi router analyzer to support dynamic method calls and improve endpoint extraction Signed-off-by: HAHWUL --- src/analyzer/analyzers/go/chi.cr | 70 ++++++++++++++------------------ 1 file changed, 30 insertions(+), 40 deletions(-) diff --git a/src/analyzer/analyzers/go/chi.cr b/src/analyzer/analyzers/go/chi.cr index e1947913..61a80879 100644 --- a/src/analyzer/analyzers/go/chi.cr +++ b/src/analyzer/analyzers/go/chi.cr @@ -27,12 +27,11 @@ module Analyzer::Go file.each_line.with_index do |line, index| details = Details.new(PathInfo.new(path, index + 1)) - # Mount 처리: r.Mount("/prefix", mountedFunc()) - if line.includes?("r.Mount(") - if match = line.match(/r\.Mount\(\s*"([^"]+)"\s*,\s*([^(]+)\(\)/) + # Mount 처리: 객체가 무엇이든 Mount 호출 인식 + if line.includes?(".Mount(") + if match = line.match(/[a-zA-Z]\w*\.Mount\(\s*"([^"]+)"\s*,\s*([^(]+)\(\)/) mount_prefix = match[1] router_function = match[2] - # mount된 라우터 함수 내의 엔드포인트를 분석해서 mount prefix를 붙임 endpoints = analyze_router_function(path, router_function) endpoints.each do |ep| ep.url = mount_prefix + ep.url @@ -42,38 +41,37 @@ module Analyzer::Go next end - # Route 블록 처리: 블록 내 접두사 저장 - if line.includes?("r.Route(") - if match = line.match(/r\.Route\(\s*"([^"]+)"/) + # Route 블록 처리: 객체가 무엇이든 Route 호출 인식 (접두사 저장) + if line.includes?(".Route(") + if match = line.match(/[a-zA-Z]\w*\.Route\(\s*"([^"]+)"/) prefix_stack << match[1] end next end + # 헤더 호출 건너뛰기 (예: c.Header.Get("X-API-Key")) + if line.includes?(".Header.Get(") + # TODO: 생각보다 복잡해져서 나중에 구현하기 + next + end + # 블록 종료 시 접두사 제거 (단순히 '}'만 있는 줄로 처리) if line.strip == "}" && !prefix_stack.empty? prefix_stack.pop next end - # 실제 endpoint 처리: r.Get, r.Post, r.Put, r.Delete + # 실제 endpoint 처리: .Get, .Post, .Put, .Delete (임의 객체) method = "" - if line.includes?("r.Get(") - method = "GET" - elsif line.includes?("r.Post(") - method = "POST" - elsif line.includes?("r.Put(") - method = "PUT" - elsif line.includes?("r.Delete(") - method = "DELETE" + route_path = "" + if match = line.match(/[a-zA-Z]\w*\.(Get|Post|Put|Delete)\(\s*"([^"]+)"/) + method = match[1].upcase + route_path = match[2] end - if method.size > 0 - route_path = extract_route_path(line) - if route_path.size > 0 - full_route = (prefix_stack.join("") + route_path).to_s - result << Endpoint.new(full_route, method, details) - end + if method.size > 0 && route_path.size > 0 + full_route = prefix_stack.join("") + route_path + result << Endpoint.new(full_route, method, details) end end end @@ -90,19 +88,18 @@ module Analyzer::Go result end - # 기존 extract_route_path: r.Get("/path", ...) 등에서 경로 추출 + # 추출: 객체.메서드("경로", ...) 형태에서 경로 추출 def extract_route_path(line : String) : String - if match = line.match(/r\.\w+\(\s*"([^"]+)"/) + if match = line.match(/[a-zA-Z]\w*\.\w+\(\s*"([^"]+)"/) return match[1] end "" end - # 주어진 파일 내에 정의된 router 함수의 내용을 추출하여 엔드포인트 정보로 변환 + # 주어진 파일 내에 정의된 router 함수의 내용을 추출하여 엔드포인트 정보로 변환 def analyze_router_function(file_path : String, func_name : String) : Array(Endpoint) endpoints = [] of Endpoint content = File.read(file_path) - # 함수 정의가 포함되어 있는지 확인 if content.includes?("func #{func_name}(") block_started = false brace_count = 0 @@ -120,21 +117,14 @@ module Analyzer::Go brace_count -= line.count("}") details = Details.new(PathInfo.new(file_path)) method = "" - if line.includes?("r.Get(") - method = "GET" - elsif line.includes?("r.Post(") - method = "POST" - elsif line.includes?("r.Put(") - method = "PUT" - elsif line.includes?("r.Delete(") - method = "DELETE" + route_path = "" + if match = line.match(/[a-zA-Z]\w*\.(Get|Post|Put|Delete)\(\s*"([^"]+)"/) + method = match[1].upcase + route_path = match[2] end - if method.size > 0 - route_path = extract_route_path(line) - if route_path.size > 0 - endpoints << Endpoint.new(route_path, method, details) - end + if method.size > 0 && route_path.size > 0 + endpoints << Endpoint.new(route_path, method, details) end break if brace_count <= 0 @@ -144,4 +134,4 @@ module Analyzer::Go endpoints end end -end +end \ No newline at end of file From 83d6fc186f75da072d6d441c4f43e51a9dce73fd Mon Sep 17 00:00:00 2001 From: HAHWUL Date: Mon, 3 Feb 2025 23:57:29 +0900 Subject: [PATCH 7/7] Linting Signed-off-by: HAHWUL --- src/analyzer/analyzers/go/chi.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/analyzer/analyzers/go/chi.cr b/src/analyzer/analyzers/go/chi.cr index 61a80879..2e51ec48 100644 --- a/src/analyzer/analyzers/go/chi.cr +++ b/src/analyzer/analyzers/go/chi.cr @@ -96,7 +96,7 @@ module Analyzer::Go "" end - # 주어진 파일 내에 정의된 router 함수의 내용을 추출하여 엔드포인트 정보로 변환 + # 주어진 파일 내에 정의된 router 함수의 내용을 추출하여 엔드포인트 정보로 변환 def analyze_router_function(file_path : String, func_name : String) : Array(Endpoint) endpoints = [] of Endpoint content = File.read(file_path) @@ -134,4 +134,4 @@ module Analyzer::Go endpoints end end -end \ No newline at end of file +end