8000 Allow rules to "block" requests from further processing by mrheinen · Pull Request #197 · mrheinen/lophiid · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Allow rules to "block" requests from further processing #197

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
8000 Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/database.sql
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ CREATE TABLE content_rule (
responder_regex VARCHAR(1024) default '',
responder_decoder RESPONDER_DECODER_TYPE default 'NONE',
enabled BOOL DEFAULT TRUE,
block BOOL DEFAULT FALSE,
CONSTRAINT fk_content_id FOREIGN KEY(content_id) REFERENCES content(id)
);

Expand Down
19 changes: 15 additions & 4 deletions pkg/agent/http_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,10 +191,21 @@ func (h *HttpServer) catchAll(w http.ResponseWriter, r *http.Request) {
log.Printf("unable to process request: %+v", err)

if st, ok := status.FromError(err); ok {
if st.Code() == codes.ResourceExhausted {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("<html></html>"))
return
switch st.Code() {
case codes.PermissionDenied:
w.WriteHeader(http.StatusForbidden)
w.Write([]byte("<html></html>"))
return

case codes.ResourceExhausted:
w.WriteHeader(http.StatusServiceUnavailable)
w.Write([]byte("<html></html>"))
return

default:
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("<html></html>"))
return
}
} else {
w.WriteHeader(http.StatusOK)
Expand Down
104 changes: 103 additions & 1 deletion pkg/agent/http_server_test.go
8000
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,75 @@ func TestCatchAllResourceExhausted(t *testing.T) {
res := w.Result()
defer res.Body.Close()

// Verify status code is 404
// Verify status code is 503 - Service Unavailable
if res.StatusCode != http.StatusServiceUnavailable {
t.Errorf("expected status code %d, got %d", http.StatusServiceUnavailable, res.StatusCode)
}

// Verify response body
data, err := io.ReadAll(res.Body)
if err != nil {
t.Errorf("reading response body: %s", err)
}

expectedBody := "<html></html>"
if string(data) != expectedBody {
t.Errorf("expected body %q, got %q", expectedBody, string(data))
}
}

func TestCatchAllPermissionDenied(t *testing.T) {
listenAddr := "127.0.0.1:8888"

// Creat 8000 e a backend client that returns a PermissionDenied error
bc := backend.FakeBackendClient{
HandleProbeReturnError: status.Error(codes.PermissionDenied, "Rule blocks request"),
}

// Create server and test request
s := NewHttpServer(&bc, listenAddr, "127.0.0.1")
req := httptest.NewRequest(http.MethodGet, "/test", nil)
w := httptest.NewRecorder()
s.catchAll(w, req)

res := w.Result()
defer res.Body.Close()

// Verify status code is 403 - Forbidden
if res.StatusCode != http.StatusForbidden {
t.Errorf("expected status code %d, got %d", http.StatusForbidden, res.StatusCode)
}

// Verify response body
data, err := io.ReadAll(res.Body)
if err != nil {
t.Errorf("reading response body: %s", err)
}

expectedBody := "<html></html>"
if string(data) != expectedBody {
t.Errorf("expected body %q, got %q", expectedBody, string(data))
}
}

func TestCatchAllDefaultError(t *testing.T) {
listenAddr := "127.0.0.1:8888"

// Create a backend client that returns an Unavailable error (not specifically handled)
bc := backend.FakeBackendClient{
HandleProbeReturnError: status.Error(codes.Unavailable, "Service unavailable"),
}

// Create server and test request
s := NewHttpServer(&bc, listenAddr, "127.0.0.1")
req := httptest.NewRequest(http.MethodGet, "/test", nil)
w := httptest.NewRecorder()
s.catchAll(w, req)

res := w.Result()
defer res.Body.Close()

// Verify status code is 404 - Not Found (default case)
if res.StatusCode != http.StatusNotFound {
t.Errorf("expected status code %d, got %d", http.StatusNotFound, res.StatusCode)
}
Expand All @@ -126,3 +194,37 @@ func TestCatchAllResourceExhausted(t *testing.T) {
t.Errorf("expected body %q, got %q", expectedBody, string(data))
}
}

func TestCatchAllNonStatusError(t *testing.T) {
listenAddr := "127.0.0.1:8888"

// Create a backend client that returns a non-status error
bc := backend.FakeBackendClient{
HandleProbeReturnError: fmt.Errorf("regular error, not a status error"),
}

// Create server and test request
s := NewHttpServer(&bc, listenAddr, "127.0.0.1")
req := httptest.NewRequest(http.MethodGet, "/test", nil)
w := httptest.NewRecorder()
s.catchAll(w, req)

res := w.Result()
defer res.Body.Close()

// Verify status code is 200 - OK (for non-status errors)
if res.StatusCode != http.StatusOK {
t.Errorf("expected status code %d, got %d", http.StatusOK, res.StatusCode)
Comment on lines +215 to +217
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In TestCatchAllNonStatusError, returning HTTP 200 OK for a non-status error seems problematic. This suggests to clients that the request succeeded when in fact there was an error. Consider whether non-status errors should return an appropriate error code (like 500 Internal Server Error) instead.

}

// Verify response body
data, err := io.ReadAll(res.Body)
if err != nil {
t.Errorf("reading response body: %s", err)
}

expectedBody := "<html></html>"
if string(data) != expectedBody {
t.Errorf("expected body %q, got %q", expectedBody, string(data))
}
}
4 changes: 4 additions & 0 deletions pkg/backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -937,6 +937,10 @@ func (s *BackendServer) HandleProbe(ctx context.Context, req *backend_service.Ha
}
}

if matchedRule.Block {
return nil, status.Errorf(codes.PermissionDenied, "Rule blocks request")
}

sReq.ContentID = matchedRule.ContentID
sReq.RuleID = matchedRule.ID
sReq.AppID = matchedRule.AppID
Expand Down
47 changes: 44 additions & 3 deletions pkg/backend/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ import (
"github.com/jackc/pgx/v5/pgtype"
"github.com/prometheus/client_golang/prometheus"
"github.com/vingarcia/ksql"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

func GetContextWithAuthMetadata() context.Context {
Expand Down Expand Up @@ -679,9 +681,10 @@ func TestHandleProbe(t *testing.T) {
},
},
ContentRulesToReturn: []models.ContentRule{
{ID: 1, AppID: 42, Method: "GET", Port: 80, Uri: "/aa", UriMatching: "exact", ContentID: 42},
{ID: 2, AppID: 42, Method: "GET", Port: 80, Uri: "/aa", UriMatching: "exact", ContentID: 42},
{ID: 3, AppID: 1, Method: "GET", Port: 80, Uri: "/script", UriMatching: "exact", ContentID: 44},
{ID: 1, AppID: 42, Block: false, Method: "GET", Port: 80, Uri: "/aa", UriMatching: "exact", ContentID: 42},
{ID: 2, AppID: 42, Block: false, Method: "GET", Port: 80, Uri: "/aa", UriMatching: "exact", ContentID: 42},
{ID: 3, AppID: 1, Block: false, Method: "GET", Port: 80, Uri: "/script", UriMatching: "exact", ContentID: 44},
{ID: 4, AppID: 5, Block: true, Method: "GET", Port: 80, Uri: "/blocked", UriMatching: "exact", ContentID: 42},
},
}

Expand Down Expand Up @@ -846,6 +849,44 @@ func TestHandleProbe(t *testing.T) {
t.Fatalf("expected %d, got %s", testSessionId, fIpMgr.Events[0].SourceRef)
}
})

t.Run("rule blocks request", func(t *testing.T) {
// Reset limiter for this test
fakeLimiter.BoolToReturn = true
fakeLimiter.ErrorToReturn = nil

// Set request URI to match our blocking rule
probeReq.RequestUri = "/blocked"
probeReq.Request.ParsedUrl.Path = "/blocked"

// Call HandleProbe and verify it returns a PermissionDenied error
res, err := b.HandleProbe(ctx, &probeReq)

// Check that we got the expected error
if res != nil {
t.Errorf("Expected nil response but got: %v", res)
}

if err == nil {
t.Errorf("Expected error but got none")
}

// Check for the specific error code and message
statusErr, ok := status.FromError(err)
if !ok {
t.Errorf("Expected gRPC status error but got: %v", err)
}

if statusErr.Code() != codes.PermissionDenied {
t.Errorf("Expected PermissionDenied error but got: %v", statusErr.Code())
}

if statusErr.Message() != "Rule blocks request" {
t.Errorf("Expected 'Rule blocks request' in error message but got: %s", statusErr.Message())
}

// No request should be added to the queue for blocked requests
})
}

func TestProcessQueue(t *testing.T) {
Expand Down
1 change: 1 addition & 0 deletions pkg/database/models/content_rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type ContentRule struct {
UpdatedAt time.Time `ksql:"updated_at,timeNowUTC" json:"updated_at" yaml:"updated_at" doc:"Last update date of the rule"`
Alert bool `ksql:"alert" json:"alert" doc:"A bool (0 or 1) indicating if the rule should alert"`
Enabled bool `ksql:"enabled" json:"enabled" doc:"A bool (0 or 1) indicating if the rule is enabled"`
Block bool `ksql:"block" json:"block" doc:"A bool (0 or 1) indicating if requests matching the rule should be blocked"`
ExtVersion int64 `ksql:"ext_version" json:"ext_version" yaml:"ext_version" doc:"The external numerical version of the rule"`
ExtUuid string `ksql:"ext_uuid" json:"ext_uuid" yaml:"ext_uuid" doc:"The external unique ID of the rule"`
// The request purpose should indicate what the request is intended to do. It
Expand Down
11 changes: 11 additions & 0 deletions ui/src/components/container/RuleForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,16 @@
/>
</td>
</tr>
<tr>
<th>Block</th>
<td>
<CheckBox
inputId="block"
v-model="localRule.block"
:binary="true"
/>
</td>
</tr>
</table>
</div>
</div>
Expand Down Expand Up @@ -311,6 +321,7 @@ export default {
responder: "NONE",
responder_decoder: "NONE",
enabled: true,
block: false,
ports: [],
parsed: {
port_field: "",
Expand Down
0