├── .github
└── workflows
│ ├── main.yml
│ └── sonar.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── SECURITY.md
├── helm
├── .helmignore
├── Chart.yaml
├── templates
│ ├── auto.yaml
│ ├── configmap-certificate.yaml
│ ├── configmap-truststore.yaml
│ ├── eks-ingress.yaml
│ ├── hazelcast-service.yaml
│ ├── internal-capi-service.yaml
│ ├── local-ingress.yaml
│ ├── metrics.yaml
│ └── server.yaml
└── values.yaml
├── pom.xml
└── src
├── main
├── java
│ └── io
│ │ └── surisoft
│ │ └── capi
│ │ ├── CapiGateway.java
│ │ ├── builder
│ │ ├── ConsistencyRouteBuilder.java
│ │ ├── ConsulKVStoreBuilder.java
│ │ ├── DirectRouteProcessor.java
│ │ ├── ErrorRoute.java
│ │ └── KafkaProcessor.java
│ │ ├── cache
│ │ ├── CacheConfiguration.java
│ │ ├── KafkaConfig.java
│ │ └── StickySessionCacheManager.java
│ │ ├── configuration
│ │ ├── CamelStartupListener.java
│ │ ├── CapiApplicationListener.java
│ │ ├── CapiConfiguration.java
│ │ ├── CapiCorsFilter.java
│ │ ├── CapiCorsFilterStrategy.java
│ │ ├── CapiSslContextHolder.java
│ │ ├── CapiTracerConfiguration.java
│ │ ├── ConsulAutoConfiguration.java
│ │ └── UndertowErrorListener.java
│ │ ├── controller
│ │ ├── CapiErrorInterface.java
│ │ ├── ClientController.java
│ │ ├── DefinitionController.java
│ │ ├── ErrorController.java
│ │ └── PublicHealthController.java
│ │ ├── exception
│ │ ├── AuthorizationException.java
│ │ ├── CapiUndertowException.java
│ │ └── RestTemplateErrorHandler.java
│ │ ├── kafka
│ │ ├── CapiEventSerializer.java
│ │ ├── CapiInstance.java
│ │ ├── CapiKafkaEvent.java
│ │ └── CapiKafkaEventDeserializer.java
│ │ ├── metrics
│ │ ├── HealthController.java
│ │ ├── Info.java
│ │ ├── KVStore.java
│ │ ├── OpenAPIDefinition.java
│ │ ├── Routes.java
│ │ ├── Truststore.java
│ │ └── WSRoutes.java
│ │ ├── oidc
│ │ ├── Oauth2ClientManager.java
│ │ ├── Oauth2Constants.java
│ │ ├── Oauth2Exception.java
│ │ ├── SSEAuthorization.java
│ │ └── WebsocketAuthorization.java
│ │ ├── processor
│ │ ├── AuthorizationProcessor.java
│ │ ├── ContentTypeValidator.java
│ │ ├── HttpErrorProcessor.java
│ │ ├── MetricsProcessor.java
│ │ ├── OpenApiProcessor.java
│ │ ├── StickyLoadBalancer.java
│ │ ├── TenantAwareLoadBalancer.java
│ │ └── ThrottleProcessor.java
│ │ ├── schema
│ │ ├── AliasInfo.java
│ │ ├── CapiEvent.java
│ │ ├── CapiInfo.java
│ │ ├── CapiRestError.java
│ │ ├── ConsulKeyStoreEntry.java
│ │ ├── ConsulObject.java
│ │ ├── Group.java
│ │ ├── HttpMethod.java
│ │ ├── HttpProtocol.java
│ │ ├── Mapping.java
│ │ ├── MappingId.java
│ │ ├── OIDCClient.java
│ │ ├── OpaResult.java
│ │ ├── RouteDetails.java
│ │ ├── RouteDetailsEndpointInfo.java
│ │ ├── RouteEndpointInfo.java
│ │ ├── RunningTenant.java
│ │ ├── SSEClient.java
│ │ ├── Service.java
│ │ ├── ServiceMeta.java
│ │ ├── State.java
│ │ ├── StickySession.java
│ │ ├── SubscriptionGroup.java
│ │ ├── ThrottleServiceObject.java
│ │ └── WebsocketClient.java
│ │ ├── service
│ │ ├── CapiEventNotifier.java
│ │ ├── CapiTrustManager.java
│ │ ├── ConsistencyChecker.java
│ │ ├── ConsulKVStore.java
│ │ ├── ConsulNodeDiscovery.java
│ │ └── OpaService.java
│ │ ├── tracer
│ │ ├── CapiTracer.java
│ │ ├── CapiTracerServerRequestAdapter.java
│ │ ├── CapiTracerServerResponseAdapter.java
│ │ └── CapiUndertowTracer.java
│ │ ├── undertow
│ │ ├── CAPILoadBalancerProxyClient.java
│ │ ├── CAPIProxyHandler.java
│ │ ├── SSEGateway.java
│ │ └── WebsocketGateway.java
│ │ └── utils
│ │ ├── Constants.java
│ │ ├── ErrorMessage.java
│ │ ├── HttpUtils.java
│ │ ├── RouteUtils.java
│ │ ├── SSEUtils.java
│ │ ├── ServiceUtils.java
│ │ └── WebsocketUtils.java
└── resources
│ ├── application.yaml
│ ├── capi.txt
│ └── logback-spring.xml
└── test
├── java
└── io
│ └── surisoft
│ └── capi
│ ├── configuration
│ ├── CapiCorsFilterStrategyTest.java
│ └── CapiCorsFilterTest.java
│ ├── controller
│ ├── CapiErrorInterfaceTest.java
│ ├── ErrorControllerTest.java
│ ├── TestCapiConfiguration.java
│ ├── TestCertificateController.java
│ ├── TestConsulAutoConfiguration.java
│ ├── TestConsulNodeDiscovery.java
│ ├── TestHttpUtils.java
│ ├── TestLoadBalancer.java
│ ├── TestRouteUtils.java
│ └── TestServiceUtils.java
│ ├── processor
│ └── OpenApiProcessorTest.java
│ └── schema
│ ├── AliasInfoTest.java
│ ├── CapiInfoTest.java
│ ├── CapiRestErrorTest.java
│ ├── ConsulObjectTest.java
│ ├── MappingIdTest.java
│ ├── MappingTest.java
│ ├── OIDCClientTest.java
│ ├── RouteDetailsEndpointInfoTest.java
│ ├── RouteDetailsTest.java
│ ├── RouteEndpointInfoTest.java
│ ├── RunningTenantTest.java
│ ├── ServiceMetaTest.java
│ ├── ServiceTest.java
│ ├── StickySessionTest.java
│ └── WebsocketClientTest.java
└── resources
├── application.yaml
├── logback-test.xml
├── test-cert-application.properties
├── test-consul-application.properties
├── test-consul-kv-application.properties
├── test-observability-application.properties
└── test-openapi-application.properties
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: capi
2 | on:
3 | push:
4 | branches: [ master ]
5 | pull_request:
6 | branches: [ master ]
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Set up JDK 21
12 | uses: actions/setup-java@v1.4.4
13 | with:
14 | java-version: 21
15 | - uses: actions/checkout@v3.5.3
16 |
17 | - name: Set up Maven settings
18 | run: |
19 | mkdir -p ~/.m2
20 | echo "github${{ github.actor }}${{ secrets.GITHUB_TOKEN }}" > ~/.m2/settings.xml
21 | - name: Set Release version env variable
22 | run: |
23 | echo "RELEASE_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)" >> $GITHUB_ENV
24 | - name: Build with Maven
25 | run: mvn package -DskipTests --file pom.xml
26 | - name: Docker login
27 | run: |
28 | docker login -u surisoft -p ${{ secrets.DOCKER_HUB_PWD }}
29 | - name: Build and push multi-platform image
30 | run: |
31 | docker buildx create --use
32 | docker buildx build . \
33 | --platform linux/amd64,linux/arm64 \
34 | --build-arg "CAPI_VERSION=${{ env.RELEASE_VERSION }}" \
35 | --file Dockerfile \
36 | --tag surisoft/capi:${{ env.RELEASE_VERSION }} \
37 | --tag surisoft/capi:latest \
38 | --push
39 |
--------------------------------------------------------------------------------
/.github/workflows/sonar.yml:
--------------------------------------------------------------------------------
1 | name: Sonar Scan
2 | on:
3 | push:
4 | branches:
5 | - master
6 | - 4.3.02
7 | pull_request:
8 | types: [opened, synchronize, reopened]
9 | jobs:
10 | build:
11 | name: Build
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v3.5.3
15 | with:
16 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
17 | - name: Set up JDK 19
18 | uses: actions/setup-java@v1.4.4
19 | with:
20 | java-version: 19
21 | - name: Cache SonarCloud packages
22 | uses: actions/cache@v1
23 | with:
24 | path: ~/.sonar/cache
25 | key: ${{ runner.os }}-sonar
26 | restore-keys: ${{ runner.os }}-sonar
27 | - name: Cache Maven packages
28 | uses: actions/cache@v1
29 | with:
30 | path: ~/.m2
31 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
32 | restore-keys: ${{ runner.os }}-m2
33 | - name: Build and analyze
34 | env:
35 | GITHUB_TOKEN: ${{ secrets.REPO_TOKEN }} # Needed to get PR information, if any
36 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
37 | run: mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=surisoft-io_capi-lb
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | target
3 | *.iml
4 |
5 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM eclipse-temurin:21-jdk-alpine
2 |
3 | ARG CAPI_VERSION=4.8.20
4 |
5 | RUN mkdir /capi
6 | RUN mkdir /capi/logs
7 |
8 | ARG JAR_FILE=target/capi-${CAPI_VERSION}.jar
9 | COPY ${JAR_FILE} /capi/app.jar
10 |
11 | ENTRYPOINT exec java -XX:InitialHeapSize=512m \
12 | -XX:+UseG1GC \
13 | -XX:MaxGCPauseMillis=100 \
14 | -XX:+ParallelRefProcEnabled \
15 | -XX:+HeapDumpOnOutOfMemoryError \
16 | -XX:HeapDumpPath=/capi/logs/heap-dump.hprof \
17 | -jar /capi/app.jar
18 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | Currently supported versions:
6 |
7 | | Version | Supported |
8 | | ------- | ------------------ |
9 | | 4.4.03 | :white_check_mark: |
10 | | 4.4.04 | :white_check_mark: |
11 | | < 4.0 | :x: |
12 |
13 | ## Reporting a Vulnerability
14 |
15 | If you find any vulnerability or bug, please, open an issue, and we will contact you back.
16 | You can also send us an email to info@surisoft.io
17 | Thanks.
18 |
--------------------------------------------------------------------------------
/helm/.helmignore:
--------------------------------------------------------------------------------
1 | # Patterns to ignore when building packages.
2 | # This supports shell glob matching, relative path matching, and
3 | # negation (prefixed with !). Only one pattern per line.
4 | .DS_Store
5 | # Common VCS dirs
6 | .git/
7 | .gitignore
8 | .bzr/
9 | .bzrignore
10 | .hg/
11 | .hgignore
12 | .svn/
13 | # Common backup files
14 | *.swp
15 | *.bak
16 | *.tmp
17 | *.orig
18 | *~
19 | # Various IDEs
20 | .project
21 | .idea/
22 | *.tmproj
23 | .vscode/
24 |
--------------------------------------------------------------------------------
/helm/Chart.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v2
2 | name: capi
3 | description: CAPI Helm chart for Kubernetes
4 | type: application
5 | version: 0.1.0
6 | appVersion: "1.0.0"
7 |
--------------------------------------------------------------------------------
/helm/templates/auto.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: autoscaling/v2
2 | kind: HorizontalPodAutoscaler
3 | metadata:
4 | namespace: {{ .Values.capi.namespace }}
5 | name: {{ .Values.capi.name }}
6 | spec:
7 | scaleTargetRef:
8 | apiVersion: apps/v1
9 | kind: Deployment
10 | name: {{ .Values.capi.name }}
11 | minReplicas: 3
12 | maxReplicas: 30
13 | behavior:
14 | scaleUp:
15 | stabilizationWindowSeconds: 60
16 | policies:
17 | - type: Pods
18 | value: 1
19 | periodSeconds: 30
20 | selectPolicy: Max
21 | scaleDown:
22 | stabilizationWindowSeconds: 90
23 | policies:
24 | - type: Pods
25 | value: 1
26 | periodSeconds: 30
27 | selectPolicy: Max
28 | metrics:
29 | - type: Resource
30 | resource:
31 | name: cpu
32 | target:
33 | type: Utilization
34 | averageUtilization: 60
35 | - type: Resource
36 | resource:
37 | name: memory
38 | target:
39 | type: Utilization
40 | averageUtilization: 70
--------------------------------------------------------------------------------
/helm/templates/configmap-certificate.yaml:
--------------------------------------------------------------------------------
1 | {{ if .Values.server.ssl.enabled }}
2 | apiVersion: v1
3 | binaryData:
4 | {{ .Values.certificate.name }}: {{ .Values.certificate.encoded }}
5 | kind: ConfigMap
6 | metadata:
7 | namespace: {{ .Values.namespace }}
8 | name: capi-certificate
9 | {{ end }}
10 |
--------------------------------------------------------------------------------
/helm/templates/configmap-truststore.yaml:
--------------------------------------------------------------------------------
1 | {{ if .Values.capi.trust.store.enabled }}
2 | apiVersion: v1
3 | binaryData:
4 | {{ .Values.trust.store.name}}: {{ .Values.trust.store.encoded}}
5 | kind: ConfigMap
6 | metadata:
7 | namespace: {{ .Values.namespace }}
8 | name: truststore
9 | {{ end }}
10 |
--------------------------------------------------------------------------------
/helm/templates/eks-ingress.yaml:
--------------------------------------------------------------------------------
1 | {{ if .Values.capi.deployment.eks }}
2 | apiVersion: networking.k8s.io/v1
3 | kind: Ingress
4 | metadata:
5 | namespace: {{ .Values.capi.namespace }}
6 | labels:
7 | service: {{ .Values.capi.name }}
8 | name: {{ .Values.capi.name }}
9 | annotations:
10 | alb.ingress.kubernetes.io/certificate-arn: {{ .Values.ssl.certificate}}
11 | alb.ingress.kubernetes.io/subnets: {{ .Values.network.subnet }}
12 | alb.ingress.kubernetes.io/security-groups: {{ .Values.network.security.group }}
13 | alb.ingress.kubernetes.io/scheme: {{ .Values.network.scheme }}
14 | external-dns.alpha.kubernetes.io/hostname: {{ .Values.network.host }}
15 | alb.ingress.kubernetes.io/load-balancer-name: {{ .Values.network.loadbalancer }}
16 | alb.ingress.kubernetes.io/backend-protocol: {{ .Values.network.backend.protocol }}
17 | alb.ingress.kubernetes.io/target-type: {{ .Values.network.target.ip }}
18 | alb.ingress.kubernetes.io/healthcheck-path: /health
19 | alb.ingress.kubernetes.io/success-codes: "200"
20 | alb.ingress.kubernetes.io/listen-port: '[{"HTTPS": 443}]'
21 | spec:
22 | ingressClassName: alb
23 | tls:
24 | - hosts:
25 | - {{ .Values.network.host }}
26 | rules:
27 | - host: {{ .Values.network.host }}
28 | http:
29 | paths:
30 | - path: /
31 | pathType: Prefix
32 | backend:
33 | service:
34 | name: {{ .Values.capi.name }}
35 | port:
36 | number: 8380
37 | {{ end }}
--------------------------------------------------------------------------------
/helm/templates/hazelcast-service.yaml:
--------------------------------------------------------------------------------
1 | {{ if .Values.capi.throttling.enabled }}
2 | apiVersion: v1
3 | kind: Service
4 | metadata:
5 | name: hazelcast-service
6 | namespace: {{ .Values.capi.namespace }}
7 | spec:
8 | clusterIP: None
9 | selector:
10 | app: {{ .Values.capi.name }}
11 | ports:
12 | - port: 5701
13 | targetPort: 5701
14 | {{ end }}
--------------------------------------------------------------------------------
/helm/templates/internal-capi-service.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | namespace: {{ .Values.capi.namespace }}
5 | labels:
6 | service: {{ .Values.capi.name }}
7 | name: {{ .Values.capi.name }}
8 | annotations:
9 | spec:
10 | ports:
11 | - name: http
12 | port: 8380
13 | targetPort: 8380
14 | type: ClusterIP
15 | selector:
16 | service: {{ .Values.capi.name }}
--------------------------------------------------------------------------------
/helm/templates/local-ingress.yaml:
--------------------------------------------------------------------------------
1 | {{ if .Values.capi.deployment.local }}
2 | apiVersion: networking.k8s.io/v1
3 | kind: Ingress
4 | metadata:
5 | namespace: {{ .Values.capi.namespace }}
6 | name: {{ .Values.capi.name }}-ingress
7 | spec:
8 | rules:
9 | - host: ingress.local
10 | http:
11 | paths:
12 | - path: /
13 | pathType: Prefix
14 | backend:
15 | service:
16 | name: {{ .Values.capi.name }}
17 | port:
18 | number: 8380
19 | {{ end }}
--------------------------------------------------------------------------------
/helm/templates/metrics.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | namespace: {{ .Values.capi.namespace }}
5 | labels:
6 | service: capi
7 | name: capi-metrics
8 | spec:
9 | ports:
10 | - name: http
11 | port: 80
12 | targetPort: 8381
13 | type: ClusterIP
14 | selector:
15 | service: {{ .Values.capi.name }}
--------------------------------------------------------------------------------
/helm/templates/server.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | namespace: {{ .Values.capi.namespace }}
5 | labels:
6 | service: {{ .Values.capi.name }}
7 | name: {{ .Values.capi.name }}
8 | spec:
9 | selector:
10 | matchLabels:
11 | service: {{ .Values.capi.name }}
12 | strategy:
13 | type: Recreate
14 | template:
15 | metadata:
16 | namespace: {{ .Values.capi.namespace }}
17 | labels:
18 | service: {{ .Values.capi.name }}
19 | spec:
20 | containers:
21 | - env:
22 | - name: capi.strict
23 | value: {{ quote .Values.capi.instance.strict }}
24 | - name: capi.namespace
25 | value: {{ .Values.capi.instance.name }}
26 | - name: spring.profiles.active
27 | value: {{ quote .Values.spring.profiles.active }}
28 | - name: capi.consul.discovery.enabled
29 | value: {{ quote .Values.capi.consul.discovery.enabled }}
30 | - name: spring.servlet.multipart.max-file-size
31 | value: 80MB
32 | - name: spring.servlet.multipart.max-request-size
33 | value: 80MB
34 | - name: capi.consul.discovery.timer.interval
35 | value: {{ quote .Values.capi.consul.discovery.timer.interval }}
36 | - name: capi.consul.hosts
37 | value: {{ quote .Values.capi.consul.hosts }}
38 | ## Trust Store Configuration
39 | - name: capi.trust.store.enabled
40 | value: {{ quote .Values.capi.trust.store.enabled }}
41 | - name: capi.trust.store.path
42 | value: {{ quote .Values.capi.trust.store.path }}
43 | - name: capi.trust.store.password
44 | value: {{ quote .Values.capi.trust.store.password }}
45 | - name: capi.oauth2.provider.enabled
46 | value: {{ quote .Values.oauth2.provider.enabled }}
47 | - name: capi.oauth2.provider.keys
48 | value: {{ quote .Values.oauth2.provider.keys }}
49 | - name: capi.gateway.cors.management.enabled
50 | value: 'true'
51 | - name: capi.opa.enabled
52 | value: {{ quote .Values.opa.enabled }}
53 | - name: capi.opa.endpoint
54 | value: {{ quote .Values.opa.endpoint }}
55 | - name: camel.servlet.mapping.context-path
56 | value: {{ quote .Values.capi.context.path }}
57 | - name: capi.traces.enabled
58 | value: {{ quote .Values.capi.traces.enabled }}
59 | - name: capi.traces.endpoint
60 | value: {{ quote .Values.capi.traces.endpoint }}
61 | - name: server.undertow.accesslog.enabled
62 | value: 'false'
63 | - name: server.undertow.accesslog.rotate
64 | value: 'true'
65 | - name: server.undertow.accesslog.dir
66 | value: accesslogs
67 | - name: pod-name
68 | valueFrom:
69 | fieldRef:
70 | fieldPath: metadata.name
71 | image: {{ .Values.image.repository }}
72 | imagePullPolicy: Always
73 | name: {{ quote .Values.capi.name }}
74 | ports:
75 | - containerPort: 8380
76 | resources:
77 | requests:
78 | memory: "512Mi"
79 | cpu: "250m"
80 | limits:
81 | memory: "2Gi"
82 | cpu: "1"
83 | volumeMounts:
84 | {{ if .Values.server.ssl.enabled }}
85 | - name: capi-certificate
86 | mountPath: /keys/capi.jks
87 | subPath: capi.jks
88 | {{ end }}
89 | {{ if .Values.capi.trust.store.enabled }}
90 | - name: truststore
91 | mountPath: /keys/truststore.jks
92 | subPath: truststore.jks
93 | {{ end }}
94 | restartPolicy: Always
95 | volumes:
96 | {{ if .Values.server.ssl.enabled }}
97 | - name: capi-certificate
98 | configMap:
99 | name: capi-certificate
100 | {{ end }}
101 | {{ if .Values.capi.trust.store.enabled }}
102 | - name: truststore
103 | configMap:
104 | name: truststore
105 | {{ end }}
106 | status: {}
107 |
108 |
--------------------------------------------------------------------------------
/helm/values.yaml:
--------------------------------------------------------------------------------
1 | replicaCount: 2
2 |
3 | image:
4 | repository: surisoft/capi:4.11.1
5 | pullPolicy: Always
6 | tag: ""
7 |
8 | ssl:
9 | certificate:
10 |
11 | network:
12 | subnet:
13 | security:
14 | group:
15 | scheme:
16 | host:
17 | loadbalancer:
18 | backend:
19 | protocol:
20 | target:
21 | ip:
22 |
23 | service:
24 | port: 8380
25 |
26 | management:
27 | port: 8381
28 | capi:
29 | instance:
30 | name: capi
31 | strict: false
32 | deployment:
33 | eks: false
34 | local: false
35 | name: capi
36 | namespace: capi-default
37 | throttling:
38 | enabled: true
39 | metrics:
40 | host: capi-metrics.capi-default.svc.cluster.local
41 | context:
42 | path: /api/*
43 | trust:
44 | store:
45 | enabled: false
46 | path:
47 | password:
48 | consul:
49 | hosts: http://host.docker.internal:8500
50 | discovery:
51 | enabled: true
52 | timer:
53 | interval: 30000
54 | traces:
55 | enabled: false
56 | endpoint:
57 |
58 | oauth2:
59 | provider:
60 | enabled: false
61 | keys:
62 |
63 | opa:
64 | enabled: false
65 | endpoint:
66 |
67 | logging:
68 | level:
69 | root: INFO
70 | io:
71 | surisoft:
72 | capi:
73 | lb: TRACE
74 | server:
75 | ssl:
76 | enabled: false
77 | key:
78 | store:
79 | type: JKS
80 | path:
81 | alias:
82 | password:
83 | spring:
84 | profiles:
85 | active: dev
86 | #Default trust store and certificate for localhost
87 | trust:
88 | store:
89 | name:
90 | encoded:
91 | certificate:
92 | name:
93 | encoded:
94 |
95 | serviceAccount:
96 | create: true
97 | annotations: {}
98 | name: ""
99 |
100 | podAnnotations: {}
101 |
102 | podSecurityContext: {}
103 |
104 | securityContext: {}
105 |
106 | resources: {}
107 |
108 | autoscaling:
109 | enabled: true
110 | minReplicas: 2
111 | maxReplicas: 10
112 | targetCPUUtilizationPercentage: 80
113 |
114 | nodeSelector: {}
115 |
116 | tolerations: []
117 |
118 | affinity: {}
119 |
--------------------------------------------------------------------------------
/src/main/java/io/surisoft/capi/CapiGateway.java:
--------------------------------------------------------------------------------
1 | package io.surisoft.capi;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 |
6 | @SpringBootApplication
7 | public class CapiGateway {
8 | public static void main(String[] args) {
9 | SpringApplication.run(CapiGateway.class, args);
10 | }
11 | }
--------------------------------------------------------------------------------
/src/main/java/io/surisoft/capi/builder/ConsistencyRouteBuilder.java:
--------------------------------------------------------------------------------
1 | package io.surisoft.capi.builder;
2 |
3 | import org.apache.camel.builder.RouteBuilder;
4 | import org.springframework.stereotype.Component;
5 |
6 | @Component
7 | public class ConsistencyRouteBuilder extends RouteBuilder {
8 | @Override
9 | public void configure() {
10 | log.debug("Creating CAPI Consistency Checker");
11 | from("timer:consistency-checker?period=20000")
12 | .to("bean:consistencyChecker?method=process")
13 | .routeId("consistency-checker-service");
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/io/surisoft/capi/builder/ConsulKVStoreBuilder.java:
--------------------------------------------------------------------------------
1 | package io.surisoft.capi.builder;
2 |
3 | import org.apache.camel.builder.RouteBuilder;
4 | import org.springframework.beans.factory.annotation.Value;
5 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
6 | import org.springframework.stereotype.Component;
7 |
8 | @Component
9 | @ConditionalOnProperty(prefix = "capi.consul.kv", name = "enabled", havingValue = "true")
10 | public class ConsulKVStoreBuilder extends RouteBuilder {
11 |
12 | private final int interval;
13 |
14 | public ConsulKVStoreBuilder(@Value("${capi.consul.kv.timer.interval}") int interval) {
15 | this.interval = interval;
16 | }
17 |
18 | @Override
19 | public void configure() throws Exception {
20 | log.debug("Creating CAPI Consul KV Store");
21 | from("timer:consul-KV-Store?period=" + interval)
22 | .to("bean:consulKVStore?method=process")
23 | .routeId("consul-key-value-store");
24 | }
25 | }
--------------------------------------------------------------------------------
/src/main/java/io/surisoft/capi/builder/ErrorRoute.java:
--------------------------------------------------------------------------------
1 | package io.surisoft.capi.builder;
2 |
3 | import io.surisoft.capi.schema.CapiRestError;
4 | import io.surisoft.capi.utils.Constants;
5 | import io.surisoft.capi.utils.HttpUtils;
6 | import org.apache.camel.Exchange;
7 | import org.apache.camel.Processor;
8 | import org.apache.camel.builder.RouteBuilder;
9 | import org.springframework.stereotype.Component;
10 |
11 | @Component
12 | public class ErrorRoute extends RouteBuilder {
13 |
14 | private final HttpUtils httpUtils;
15 |
16 | public ErrorRoute(HttpUtils httpUtils) {
17 | this.httpUtils = httpUtils;
18 | }
19 |
20 | @Override
21 | public void configure() {
22 | from("direct:error")
23 | .setHeader("Content-Type", constant("application/json"))
24 | .process(new Processor() {
25 | @Override
26 | public void process(Exchange exchange) throws Exception {
27 | CapiRestError capiRestError = new CapiRestError();
28 | if(exchange.getIn().getHeader(Constants.CAPI_URI_IN_ERROR) != null) {
29 | capiRestError.setHttpUri(exchange.getIn().getHeader(Constants.CAPI_URI_IN_ERROR, String.class));
30 | }
31 | if(exchange.getIn().getHeader(Constants.ROUTE_ID_HEADER) != null) {
32 | capiRestError.setRouteID(exchange.getIn().getHeader(Constants.ROUTE_ID_HEADER, String.class));
33 | }
34 | if(exchange.getIn().getHeader("x-b3-traceid") != null || exchange.getIn().getHeader("X-B3-Traceid", String.class) != null) {
35 | capiRestError.setTraceID(exchange.getIn().getHeader(Constants.TRACE_ID_HEADER, String.class));
36 | }
37 | if(exchange.getIn().getHeader(Constants.REASON_MESSAGE_HEADER) != null && exchange.getIn().getHeader(Constants.REASON_CODE_HEADER) != null) {
38 | exchange.setProperty("serviceResponseCode", exchange.getIn().getHeader(Constants.REASON_CODE_HEADER));
39 | capiRestError.setErrorMessage(exchange.getIn().getHeader(Constants.REASON_MESSAGE_HEADER, String.class));
40 | capiRestError.setErrorCode(exchange.getIn().getHeader(Constants.REASON_CODE_HEADER, Integer.class));
41 | } else {
42 | exchange.setProperty("serviceResponseCode", 400);
43 | capiRestError.setErrorMessage("Unknown error");
44 | capiRestError.setErrorCode(400);
45 | }
46 | exchange.getIn().setBody(httpUtils.proxyErrorMapper(capiRestError));
47 | }
48 | })
49 | .setHeader(Exchange.HTTP_RESPONSE_CODE, exchangeProperty("serviceResponseCode"))
50 | .removeHeader(Constants.REASON_MESSAGE_HEADER)
51 | .removeHeader(Constants.REASON_CODE_HEADER)
52 | .routeId("error-route")
53 | .end();
54 | }
55 | }
--------------------------------------------------------------------------------
/src/main/java/io/surisoft/capi/builder/KafkaProcessor.java:
--------------------------------------------------------------------------------
1 | package io.surisoft.capi.builder;
2 |
3 | import org.apache.camel.builder.RouteBuilder;
4 | import org.springframework.beans.factory.annotation.Value;
5 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
6 | import org.springframework.stereotype.Component;
7 |
8 | @Component
9 | @ConditionalOnProperty(prefix = "capi.kafka", name = "enabled", havingValue = "true")
10 | public class KafkaProcessor extends RouteBuilder {
11 |
12 | @Value("${capi.kafka.host}")
13 | private String capiKafkaHost;
14 |
15 | @Value("${capi.kafka.topic}")
16 | private String capiKafkaTopic;
17 |
18 | @Value("${capi.kafka.group-instance}")
19 | private String capiKafkaGroupInstance;
20 |
21 | @Value("${capi.kafka.group-id}")
22 | private String capiKafkaGroupId;
23 |
24 | @Value("${capi.kafka.ssl.enabled}")
25 | private boolean capiKafkaSslEnabled;
26 |
27 | @Value("${capi.kafka.ssl.keystore.location}")
28 | private String capiKafkaSslKeystoreLocation;
29 |
30 | @Value("${capi.kafka.ssl.keystore.password}")
31 | private String capiKafkaSslKeystorePassword;
32 |
33 | @Value("${capi.kafka.ssl.truststore.location}")
34 | private String capiKafkaSslTruststoreLocation;
35 |
36 | @Value("${capi.kafka.ssl.truststore.password}")
37 | private String capiKafkaSslTruststorePassword;
38 |
39 | @Override
40 | public void configure() {
41 | from("kafka:" + buildEndpoint()).to("bean:capiKafkaEventProcessor?method=process(${body})");
42 | }
43 |
44 | private String buildEndpoint() {
45 | if(capiKafkaSslEnabled) {
46 | return capiKafkaTopic +
47 | "?brokers=" + capiKafkaHost +
48 | "&securityProtocol=SSL" +
49 | "&sslKeystoreLocation=" + capiKafkaSslKeystoreLocation +
50 | "&sslKeystorePassword=" + capiKafkaSslKeystorePassword +
51 | "&sslKeyPassword=" + capiKafkaSslKeystorePassword +
52 | "&sslTruststoreLocation=" + capiKafkaSslTruststoreLocation +
53 | "&sslTruststorePassword=" + capiKafkaSslTruststorePassword +
54 | "&groupInstanceId=" + capiKafkaGroupInstance +
55 | "&autoOffsetReset=latest" +
56 | "&groupId=" + capiKafkaGroupId +
57 | "&valueDeserializer=io.surisoft.capi.kafka.CapiKafkaEventDeserializer";
58 | } else {
59 | return capiKafkaTopic +
60 | "?brokers=" + capiKafkaHost +
61 | "&groupId=" + capiKafkaGroupId +
62 | "&autoOffsetReset=latest" +
63 | "&consumersCount=1" +
64 | "&valueDeserializer=io.surisoft.capi.kafka.CapiKafkaEventDeserializer";
65 | }
66 | }
67 | }
--------------------------------------------------------------------------------
/src/main/java/io/surisoft/capi/cache/CacheConfiguration.java:
--------------------------------------------------------------------------------
1 | package io.surisoft.capi.cache;
2 |
3 | import io.surisoft.capi.schema.ConsulKeyStoreEntry;
4 | import io.surisoft.capi.schema.Service;
5 | import io.surisoft.capi.schema.StickySession;
6 | import io.surisoft.capi.schema.ThrottleServiceObject;
7 | import io.surisoft.capi.utils.Constants;
8 | import org.cache2k.Cache;
9 | import org.cache2k.Cache2kBuilder;
10 | import org.slf4j.Logger;
11 | import org.slf4j.LoggerFactory;
12 | import org.springframework.beans.factory.annotation.Value;
13 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
14 | import org.springframework.context.annotation.Bean;
15 | import org.springframework.context.annotation.Configuration;
16 | import org.springframework.http.ResponseEntity;
17 | import org.springframework.web.client.RestTemplate;
18 |
19 | import java.util.*;
20 |
21 | @Configuration
22 | public class CacheConfiguration {
23 |
24 | private static final Logger log = LoggerFactory.getLogger(CacheConfiguration.class);
25 | private final List allowedHeaders;
26 | private final List capiConsulHosts;
27 |
28 | public CacheConfiguration(@Value("${capi.gateway.cors.management.allowed-headers}") List allowedHeaders,
29 | @Value("${capi.consul.hosts}") List capiConsulHosts) {
30 | this.allowedHeaders = allowedHeaders;
31 | this.capiConsulHosts = capiConsulHosts;
32 | }
33 |
34 | @Bean
35 | public Cache serviceCache() {
36 | log.debug("Creating Service Cache");
37 | return new Cache2kBuilder(){}
38 | .name("serviceCache-" + hashCode())
39 | .eternal(true)
40 | .entryCapacity(10000)
41 | .storeByReference(true)
42 | .build();
43 | }
44 |
45 | @Bean
46 | public Cache stickySessionCache() {
47 | log.debug("Creating Service Cache");
48 | return new Cache2kBuilder(){}
49 | .name("stickySessionCache-" + hashCode())
50 | .eternal(true)
51 | .entryCapacity(10000)
52 | .storeByReference(true)
53 | .build();
54 | }
55 |
56 | @Bean
57 | @ConditionalOnProperty(prefix = "capi.consul.kv", name = "enabled", havingValue = "true")
58 | public Cache> consulKvStoreCache(RestTemplate restTemplate) {
59 | String consulHost = capiConsulHosts.get(0);
60 | log.debug("Creating Consul KV Cache");
61 | Cache> consulKvStoreCache = new Cache2kBuilder>(){}
62 | .name("consulKvStoreCache-" + hashCode())
63 | .eternal(true)
64 | .entryCapacity(10000)
65 | .storeByReference(true)
66 | .build();
67 |
68 | //Processing CORS Headers
69 | log.info("Checking Consul Key Store for CORS Headers key/values");
70 | try {
71 | ResponseEntity consulKeyValueStoreResponse = restTemplate.getForEntity(consulHost + Constants.CONSUL_KV_STORE_API + Constants.CAPI_CORS_HEADERS_CACHE_KEY, ConsulKeyStoreEntry[].class);
72 | if(!consulKeyValueStoreResponse.getStatusCode().is2xxSuccessful()) {
73 | consulKvStoreCache.put(Constants.CAPI_CORS_HEADERS_CACHE_KEY, allowedHeaders);
74 | } else {
75 | ConsulKeyStoreEntry consulKeyValueStore = Objects.requireNonNull(consulKeyValueStoreResponse.getBody())[0];
76 | List consulDecodedValueAsList = consulKeyValueAsList(consulKeyValueStore.getValue());
77 | consulKvStoreCache.put(Constants.CAPI_CORS_HEADERS_CACHE_KEY, consulDecodedValueAsList);
78 | }
79 | } catch(Exception e) {
80 | consulKvStoreCache.put(Constants.CAPI_CORS_HEADERS_CACHE_KEY, allowedHeaders);
81 | }
82 | return consulKvStoreCache;
83 | }
84 |
85 | @Bean
86 | @ConditionalOnProperty(prefix = "capi.throttling", name = "enabled", havingValue = "true")
87 | public Cache createLocalThrottleCache() {
88 | log.debug("Creating Throttle Cache");
89 | return new Cache2kBuilder(){}
90 | .name("throttleServiceObject-" + hashCode())
91 | .eternal(true)
92 | .entryCapacity(10000)
93 | .storeByReference(true)
94 | .build();
95 | }
96 |
97 | private List consulKeyValueAsList(String encodedValue) {
98 | String decodedValue = new String(Base64.getDecoder().decode(encodedValue));
99 | return Arrays.asList(decodedValue.split(",", -1));
100 | }
101 | }
--------------------------------------------------------------------------------
/src/main/java/io/surisoft/capi/cache/KafkaConfig.java:
--------------------------------------------------------------------------------
1 | package io.surisoft.capi.cache;
2 |
3 | import io.surisoft.capi.kafka.CapiEventSerializer;
4 | import io.surisoft.capi.kafka.CapiInstance;
5 | import io.surisoft.capi.kafka.CapiKafkaEvent;
6 | import io.surisoft.capi.schema.CapiEvent;
7 | import org.apache.kafka.clients.producer.ProducerConfig;
8 | import org.apache.kafka.common.serialization.StringSerializer;
9 | import org.slf4j.Logger;
10 | import org.slf4j.LoggerFactory;
11 | import org.springframework.beans.factory.annotation.Autowired;
12 | import org.springframework.beans.factory.annotation.Value;
13 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
14 | import org.springframework.context.annotation.Bean;
15 | import org.springframework.context.annotation.Configuration;
16 | import org.springframework.kafka.core.DefaultKafkaProducerFactory;
17 | import org.springframework.kafka.core.KafkaTemplate;
18 | import org.springframework.kafka.core.ProducerFactory;
19 |
20 | import java.util.HashMap;
21 | import java.util.Map;
22 | import java.util.UUID;
23 |
24 | import static org.apache.kafka.clients.CommonClientConfigs.SECURITY_PROTOCOL_CONFIG;
25 | import static org.apache.kafka.common.config.SslConfigs.*;
26 |
27 | @Configuration
28 | @ConditionalOnProperty(prefix = "capi.kafka", name = "enabled", havingValue = "true")
29 | public class KafkaConfig {
30 |
31 | private static final Logger log = LoggerFactory.getLogger(KafkaConfig.class);
32 |
33 | @Value("${capi.kafka.host}")
34 | private String capiKafkaHost;
35 |
36 | //@Value("${capi.kafka.topic}")
37 | //private String capiKafkaTopic;
38 |
39 | //@Value("${capi.kafka.group-instance}")
40 | //private String capiKafkaGroupInstance;
41 |
42 | //@Value("${capi.kafka.group-id}")
43 | //private String capiKafkaGroupId;
44 |
45 | @Value("${capi.kafka.ssl.enabled}")
46 | private boolean capiKafkaSslEnabled;
47 |
48 | @Value("${capi.kafka.ssl.keystore.location}")
49 | private String capiKafkaSslKeystoreLocation;
50 |
51 | @Value("${capi.kafka.ssl.keystore.password}")
52 | private String capiKafkaSslKeystorePassword;
53 |
54 | @Value("${capi.kafka.ssl.truststore.location}")
55 | private String capiKafkaSslTruststoreLocation;
56 |
57 | @Value("${capi.kafka.ssl.truststore.password}")
58 | private String capiKafkaSslTruststorePassword;
59 |
60 | @Bean
61 | public CapiInstance capiInstance() {
62 | return new CapiInstance(UUID.randomUUID().toString());
63 | }
64 |
65 | @Bean
66 | @ConditionalOnProperty(prefix = "capi.kafka", name = "enabled", havingValue = "true")
67 | public ProducerFactory producerFactory() {
68 | log.info("Configuring CAPI Kafka Producer");
69 | Map configProps = new HashMap<>();
70 | configProps.put(
71 | ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,
72 | capiKafkaHost);
73 | configProps.put(
74 | ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
75 | StringSerializer.class);
76 | configProps.put(
77 | ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
78 | CapiEventSerializer.class);
79 | if(capiKafkaSslEnabled) {
80 | configProps.put(
81 | SECURITY_PROTOCOL_CONFIG,
82 | "SSL");
83 | configProps.put(SSL_TRUSTSTORE_LOCATION_CONFIG,
84 | capiKafkaSslTruststoreLocation);
85 | configProps.put(SSL_TRUSTSTORE_PASSWORD_CONFIG,
86 | capiKafkaSslTruststorePassword);
87 | configProps.put(SSL_KEYSTORE_LOCATION_CONFIG,
88 | capiKafkaSslKeystoreLocation);
89 | configProps.put(SSL_KEYSTORE_PASSWORD_CONFIG,
90 | capiKafkaSslKeystorePassword);
91 | configProps.put(SSL_KEY_PASSWORD_CONFIG,
92 | capiKafkaSslKeystorePassword);
93 | }
94 | return new DefaultKafkaProducerFactory<>(configProps);
95 | }
96 |
97 | @Bean
98 | public KafkaTemplate kafkaTemplate() {
99 | return new KafkaTemplate<>(producerFactory());
100 | }
101 | }
--------------------------------------------------------------------------------
/src/main/java/io/surisoft/capi/cache/StickySessionCacheManager.java:
--------------------------------------------------------------------------------
1 | package io.surisoft.capi.cache;
2 |
3 | import io.surisoft.capi.kafka.CapiInstance;
4 | import io.surisoft.capi.kafka.CapiKafkaEvent;
5 | import io.surisoft.capi.schema.CapiEvent;
6 | import io.surisoft.capi.schema.StickySession;
7 | import org.cache2k.Cache;
8 | import org.springframework.beans.factory.annotation.Value;
9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
10 | import org.springframework.kafka.core.KafkaTemplate;
11 | import org.springframework.stereotype.Component;
12 |
13 | import java.util.UUID;
14 |
15 | @Component
16 | @ConditionalOnProperty(value = "capi.kafka.enabled", havingValue = "true")
17 | public class StickySessionCacheManager {
18 | private final Cache stickySessionCache;
19 | private final KafkaTemplate kafkaTemplate;
20 | private final CapiInstance capiInstance;
21 | private final String capiKafkaTopic;
22 |
23 | public StickySessionCacheManager(Cache stickySessionCache,
24 | KafkaTemplate kafkaTemplate,
25 | CapiInstance capiInstance,
26 | @Value("${capi.kafka.topic}") String capiKafkaTopic) {
27 | this.stickySessionCache = stickySessionCache;
28 | this.kafkaTemplate = kafkaTemplate;
29 | this.capiInstance = capiInstance;
30 | this.capiKafkaTopic = capiKafkaTopic;
31 | }
32 |
33 | public void createStickySession(StickySession stickySession, boolean notifyOtherInstances) {
34 | notifyOtherInstances(notifyOtherInstances, stickySession);
35 | stickySession.setId(stickySession.getParamName() + ":" + stickySession.getParamValue());
36 | stickySessionCache.put(stickySession.getId(), stickySession);
37 | }
38 |
39 | public StickySession getStickySessionById(String paramName, String paramValue) {
40 | String stickySessionId = paramName + ":" + paramValue;
41 | return stickySessionCache.peek(stickySessionId);
42 | }
43 |
44 | public void deleteStickySession(StickySession stickySession) {
45 | stickySessionCache.remove(stickySession.getId());
46 | }
47 |
48 | private void notifyOtherInstances(boolean notifyOtherInstances, StickySession stickySession) {
49 | if(notifyOtherInstances) {
50 | CapiEvent capiEvent = new CapiEvent();
51 | capiEvent.setId(UUID.randomUUID().toString());
52 | //capiEvent.setInstanceId(capiInstance);
53 | capiEvent.setKey(stickySession.getParamName());
54 | capiEvent.setValue(stickySession.getParamValue());
55 | capiEvent.setNodeIndex(stickySession.getNodeIndex());
56 | capiEvent.setType(CapiKafkaEvent.STICKY_SESSION_EVENT_TYPE);
57 | kafkaTemplate.send(capiKafkaTopic, capiEvent);
58 | }
59 | }
60 | }
--------------------------------------------------------------------------------
/src/main/java/io/surisoft/capi/configuration/CamelStartupListener.java:
--------------------------------------------------------------------------------
1 | package io.surisoft.capi.configuration;
2 |
3 | import org.apache.camel.CamelContext;
4 | import org.apache.camel.ExtendedStartupListener;
5 | import org.apache.camel.builder.RouteBuilder;
6 | import org.slf4j.Logger;
7 | import org.slf4j.LoggerFactory;
8 |
9 | public class CamelStartupListener implements ExtendedStartupListener {
10 |
11 | private static final Logger log = LoggerFactory.getLogger(CamelStartupListener.class);
12 | private final long consulTimerInterval;
13 | private final boolean capiConsulEnabled;
14 |
15 | public CamelStartupListener(long consulTimerInterval, boolean capiConsulEnabled) {
16 | this.consulTimerInterval = consulTimerInterval;
17 | this.capiConsulEnabled = capiConsulEnabled;
18 | }
19 |
20 | @Override
21 | public void onCamelContextStarted(CamelContext context, boolean alreadyStarted) throws Exception {
22 | }
23 |
24 | @Override
25 | public void onCamelContextFullyStarted(CamelContext context, boolean alreadyStarted) throws Exception {
26 | if(capiConsulEnabled) {
27 | context.addRoutes(routeBuilder());
28 | }
29 | }
30 |
31 | public RouteBuilder routeBuilder() {
32 | log.debug("Creating Capi Consul Node Discovery");
33 | return new RouteBuilder() {
34 | @Override
35 | public void configure() {
36 | from("timer:consul-inspect?period=" + consulTimerInterval)
37 | .to("bean:consulNodeDiscovery?method=processInfo")
38 | .routeId("consul-discovery-service");
39 | }
40 | };
41 | }
42 | }
--------------------------------------------------------------------------------
/src/main/java/io/surisoft/capi/configuration/CapiApplicationListener.java:
--------------------------------------------------------------------------------
1 | package io.surisoft.capi.configuration;
2 |
3 | import io.surisoft.capi.schema.Service;
4 | import io.surisoft.capi.schema.StickySession;
5 | import io.surisoft.capi.undertow.SSEGateway;
6 | import io.surisoft.capi.undertow.WebsocketGateway;
7 | import org.cache2k.Cache;
8 | import org.jetbrains.annotations.NotNull;
9 | import org.slf4j.Logger;
10 | import org.slf4j.LoggerFactory;
11 | import org.springframework.boot.context.event.ApplicationStartedEvent;
12 | import org.springframework.context.ApplicationEvent;
13 | import org.springframework.context.ApplicationListener;
14 | import org.springframework.context.event.ContextClosedEvent;
15 | import org.springframework.stereotype.Component;
16 |
17 | import java.util.Optional;
18 |
19 | @Component
20 | public class CapiApplicationListener implements ApplicationListener {
21 |
22 | private static final Logger log = LoggerFactory.getLogger(CapiApplicationListener.class);
23 | private final Cache serviceCache;
24 | private final Cache stickySessionCache;
25 | private final Optional websocketGateway;
26 | private final Optional sseGateway;
27 |
28 | public CapiApplicationListener(Cache serviceCache, Cache stickySessionCache, Optional websocketGateway, Optional sseGateway) {
29 | this.serviceCache = serviceCache;
30 | this.stickySessionCache = stickySessionCache;
31 | this.websocketGateway = websocketGateway;
32 | this.sseGateway = sseGateway;
33 | }
34 |
35 | @Override
36 | public void onApplicationEvent(@NotNull ApplicationEvent applicationEvent) {
37 | if(applicationEvent instanceof ApplicationStartedEvent) {
38 | if(websocketGateway.isPresent()) {
39 | log.info("Capi Websocket Gateway starting.");
40 | websocketGateway.get().runProxy();
41 | }
42 | if(sseGateway.isPresent()) {
43 | log.info("Capi SSE Gateway starting.");
44 | sseGateway.get().runProxy();
45 | }
46 | }
47 | if(applicationEvent instanceof ContextClosedEvent) {
48 | log.info("Capi is shutting down, time to clear all cache info.");
49 | serviceCache.clear();
50 | stickySessionCache.clear();
51 | }
52 | }
53 | }
--------------------------------------------------------------------------------
/src/main/java/io/surisoft/capi/configuration/CapiCorsFilterStrategy.java:
--------------------------------------------------------------------------------
1 | package io.surisoft.capi.configuration;
2 |
3 | import io.surisoft.capi.utils.Constants;
4 | import org.apache.camel.Exchange;
5 | import org.apache.camel.support.DefaultHeaderFilterStrategy;
6 | import org.apache.commons.lang3.StringUtils;
7 | import org.slf4j.Logger;
8 | import org.slf4j.LoggerFactory;
9 |
10 | import java.util.List;
11 | import java.util.Map;
12 |
13 | public class CapiCorsFilterStrategy extends DefaultHeaderFilterStrategy {
14 |
15 | private static final Logger log = LoggerFactory.getLogger(CapiCorsFilterStrategy.class);
16 | private final List allowedHeaders;
17 | private Map managedHeaders;
18 |
19 | public CapiCorsFilterStrategy(List allowedHeaders) {
20 | log.info("Capi Filter Strategy initialized");
21 | this.allowedHeaders = allowedHeaders;
22 | initialize();
23 | }
24 |
25 | protected void initialize() {
26 | getOutFilter().add("content-length");
27 | //getOutFilter().add("content-type");
28 | getOutFilter().add("host");
29 | getOutFilter().add("cache-control");
30 | getOutFilter().add("connection");
31 | getOutFilter().add("date");
32 | getOutFilter().add("pragma");
33 | getOutFilter().add("trailer");
34 | getOutFilter().add("transfer-encoding");
35 | getOutFilter().add("upgrade");
36 | getOutFilter().add("via");
37 | getOutFilter().add("warning");
38 | getOutFilter().add(Constants.ACCESS_CONTROL_ALLOW_ORIGIN);
39 |
40 | managedHeaders = new java.util.HashMap<>(Constants.CAPI_CORS_MANAGED_HEADERS);
41 | managedHeaders.put("Access-Control-Allow-Headers", StringUtils.join(allowedHeaders, ","));
42 | managedHeaders.forEach((key, value) -> {
43 | getOutFilter().add(key);
44 | });
45 |
46 | setLowerCase(true);
47 |
48 | // filter headers begin with "Camel" or "org.apache.camel"
49 | // must ignore case for Http based transports
50 | setOutFilterStartsWith(CAMEL_FILTER_STARTS_WITH);
51 | setInFilterStartsWith(CAMEL_FILTER_STARTS_WITH);
52 | }
53 |
54 | @Override
55 | public boolean applyFilterToExternalHeaders(String headerName, Object headerValue, Exchange exchange) {
56 | if(headerName.equalsIgnoreCase(Constants.ACCESS_CONTROL_ALLOW_ORIGIN)) {
57 | return true;
58 | }
59 | if(managedHeaders.containsKey(headerName)) {
60 | return true;
61 | }
62 | if(headerName.equalsIgnoreCase(Constants.ACCESS_CONTROL_ALLOW_METHODS)) {
63 | return true;
64 | }
65 | if(headerName.equalsIgnoreCase(Constants.ACCESS_CONTROL_ALLOW_CREDENTIALS)) {
66 | return true;
67 | }
68 | return super.applyFilterToExternalHeaders(headerName, headerValue, exchange);
69 | }
70 | }
--------------------------------------------------------------------------------
/src/main/java/io/surisoft/capi/configuration/CapiSslContextHolder.java:
--------------------------------------------------------------------------------
1 | package io.surisoft.capi.configuration;
2 |
3 | import javax.net.ssl.SSLContext;
4 |
5 | public class CapiSslContextHolder {
6 | private SSLContext sslContext;
7 | public CapiSslContextHolder(SSLContext sslContext) {
8 | this.sslContext = sslContext;
9 | }
10 | public SSLContext getSslContext() {
11 | return sslContext;
12 | }
13 | public void setSslContext(SSLContext sslContext) {
14 | this.sslContext = sslContext;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/java/io/surisoft/capi/configuration/CapiTracerConfiguration.java:
--------------------------------------------------------------------------------
1 | package io.surisoft.capi.configuration;
2 |
3 | import io.surisoft.capi.tracer.CapiTracer;
4 | import io.surisoft.capi.tracer.CapiUndertowTracer;
5 | import io.surisoft.capi.utils.HttpUtils;
6 | import org.apache.camel.CamelContext;
7 | import org.slf4j.Logger;
8 | import org.slf4j.LoggerFactory;
9 | import org.springframework.beans.factory.annotation.Value;
10 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
11 | import org.springframework.context.annotation.Bean;
12 | import org.springframework.context.annotation.Configuration;
13 | import zipkin2.reporter.AsyncReporter;
14 | import zipkin2.reporter.urlconnection.URLConnectionSender;
15 |
16 | import java.io.IOException;
17 | import java.security.KeyManagementException;
18 | import java.security.KeyStoreException;
19 | import java.security.NoSuchAlgorithmException;
20 | import java.security.cert.CertificateException;
21 | import java.util.HashSet;
22 | import java.util.Set;
23 |
24 | @Configuration
25 | @ConditionalOnProperty(prefix = "capi.traces", name = "enabled", havingValue = "true")
26 | public class CapiTracerConfiguration {
27 |
28 | private static final Logger log = LoggerFactory.getLogger(CapiTracerConfiguration.class);
29 | private final String tracesEndpoint;
30 | private final HttpUtils httpUtils;
31 |
32 | public CapiTracerConfiguration(@Value("${capi.traces.endpoint}") String tracesEndpoint,
33 | HttpUtils httpUtils) {
34 | this.tracesEndpoint = tracesEndpoint;
35 | this.httpUtils = httpUtils;
36 | }
37 |
38 | @Bean
39 | public CapiTracer capiTracer(CamelContext camelContext) throws CertificateException, IOException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
40 | camelContext.setUseMDCLogging(true);
41 |
42 | log.debug("Traces Enabled!");
43 |
44 | Set excludePatterns = new HashSet<>();
45 | excludePatterns.add("timer://");
46 | excludePatterns.add("bean://consulNodeDiscovery");
47 | excludePatterns.add("bean://consistencyChecker");
48 |
49 | CapiTracer capiTracer = new CapiTracer(httpUtils);
50 |
51 | URLConnectionSender sender = URLConnectionSender
52 | .newBuilder()
53 | .readTimeout(100)
54 | .endpoint(tracesEndpoint + "/api/v2/spans")
55 | .build();
56 |
57 | capiTracer.setSpanReporter(AsyncReporter.builder(sender).build());
58 | capiTracer.setIncludeMessageBody(true);
59 | capiTracer.setIncludeMessageBodyStreams(true);
60 |
61 | capiTracer.init(camelContext);
62 | return capiTracer;
63 | }
64 |
65 | @Bean
66 | public CapiUndertowTracer capiUndertowTracer() throws Exception {
67 | log.debug("Undertow Traces Enabled!");
68 |
69 | CapiUndertowTracer capiUndertowTracer = new CapiUndertowTracer(httpUtils);
70 |
71 | URLConnectionSender sender = URLConnectionSender
72 | .newBuilder()
73 | .readTimeout(100)
74 | .endpoint(tracesEndpoint + "/api/v2/spans")
75 | .build();
76 |
77 | capiUndertowTracer.setSpanReporter(AsyncReporter.builder(sender).build());
78 | capiUndertowTracer.init();
79 | return capiUndertowTracer;
80 | }
81 | }
--------------------------------------------------------------------------------
/src/main/java/io/surisoft/capi/configuration/UndertowErrorListener.java:
--------------------------------------------------------------------------------
1 | package io.surisoft.capi.configuration;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import io.surisoft.capi.schema.CapiRestError;
5 | import io.surisoft.capi.utils.Constants;
6 | import io.undertow.Undertow;
7 | import io.undertow.util.HeaderMap;
8 | import org.springframework.beans.factory.annotation.Value;
9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
10 | import org.springframework.http.MediaType;
11 | import org.springframework.stereotype.Component;
12 |
13 | @Component
14 | @ConditionalOnProperty(prefix = "capi.gateway.error.listener", name = "enabled", havingValue = "true")
15 | public class UndertowErrorListener {
16 | private final ObjectMapper objectMapper = new ObjectMapper();
17 | private final int listenerPort;
18 | private final String listenerContext;
19 |
20 | public UndertowErrorListener(@Value("${capi.gateway.error.listener.port}") int listenerPort,
21 | @Value("${capi.gateway.error.listener.context}") String listenerContext) {
22 | this.listenerPort = listenerPort;
23 | this.listenerContext = listenerContext;
24 | runProxy();
25 | }
26 |
27 | public void runProxy() {
28 | Undertow undertow = Undertow.builder()
29 | .addHttpListener(listenerPort, Constants.ERROR_LISTENING_ADDRESS)
30 | .setHandler(httpServerExchange -> {
31 | if(httpServerExchange.getRelativePath().startsWith(listenerContext)) {
32 | httpServerExchange.getResponseHeaders().add(Constants.HTTP_STRING_CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
33 | HeaderMap headerMap = httpServerExchange.getRequestHeaders();
34 | CapiRestError capiRestError = buildCapiErrorObject(headerMap);
35 |
36 | if(headerMap.contains(Constants.REASON_CODE_HEADER)) {
37 | httpServerExchange.setStatusCode(Integer.parseInt(headerMap.get(Constants.REASON_CODE_HEADER).get(0)));
38 | }
39 | httpServerExchange.getResponseSender().send(objectMapper.writeValueAsString(capiRestError));
40 | httpServerExchange.endExchange();
41 | } else {
42 | httpServerExchange.setStatusCode(400);
43 | httpServerExchange.getResponseSender().send("BAD REQUEST");
44 | httpServerExchange.endExchange();
45 | }
46 | }).build();
47 | undertow.start();
48 | }
49 |
50 | private CapiRestError buildCapiErrorObject(HeaderMap headerMap) {
51 | CapiRestError capiRestError = new CapiRestError();
52 | if(headerMap.contains(Constants.REASON_CODE_HEADER)) {
53 | capiRestError.setErrorCode(Integer.parseInt(headerMap.get(Constants.REASON_CODE_HEADER).get(0)));
54 | }
55 |
56 | if(headerMap.contains(Constants.REASON_MESSAGE_HEADER)) {
57 | capiRestError.setErrorMessage(headerMap.get(Constants.REASON_MESSAGE_HEADER).get(0));
58 | }
59 |
60 | if(headerMap.contains(Constants.ROUTE_ID_HEADER)) {
61 | capiRestError.setRouteID(headerMap.get(Constants.ROUTE_ID_HEADER).get(0));
62 | }
63 |
64 | if(headerMap.contains(Constants.CAPI_URI_IN_ERROR)) {
65 | capiRestError.setHttpUri(headerMap.get(Constants.CAPI_URI_IN_ERROR).get(0));
66 | }
67 |
68 | if(headerMap.contains(Constants.TRACE_ID_HEADER)) {
69 | capiRestError.setTraceID(headerMap.get(Constants.TRACE_ID_HEADER).get(0));
70 | }
71 |
72 | return capiRestError;
73 | }
74 | }
--------------------------------------------------------------------------------
/src/main/java/io/surisoft/capi/controller/CapiErrorInterface.java:
--------------------------------------------------------------------------------
1 | package io.surisoft.capi.controller;
2 |
3 | import io.surisoft.capi.utils.Constants;
4 | import jakarta.servlet.RequestDispatcher;
5 | import jakarta.servlet.http.HttpServletRequest;
6 | import org.apache.camel.util.json.JsonObject;
7 | import org.slf4j.Logger;
8 | import org.slf4j.LoggerFactory;
9 | import org.springframework.boot.web.servlet.error.ErrorController;
10 | import org.springframework.http.HttpStatus;
11 | import org.springframework.http.MediaType;
12 | import org.springframework.http.ResponseEntity;
13 | import org.springframework.stereotype.Controller;
14 | import org.springframework.web.bind.annotation.*;
15 |
16 | @Controller
17 | public class CapiErrorInterface implements ErrorController {
18 |
19 | private static final Logger log = LoggerFactory.getLogger(CapiErrorInterface.class);
20 |
21 | @GetMapping(path = "/error",
22 | produces = MediaType.APPLICATION_JSON_VALUE
23 | )
24 | public ResponseEntity handleGet(HttpServletRequest request) {
25 | return handleTheError(request);
26 | }
27 |
28 | @PostMapping(path = "/error",
29 | produces = MediaType.APPLICATION_JSON_VALUE
30 | )
31 | public ResponseEntity handlePost(HttpServletRequest request) {
32 | return handleTheError(request);
33 | }
34 |
35 | @PutMapping(path = "/error",
36 | produces = MediaType.APPLICATION_JSON_VALUE
37 | )
38 | public ResponseEntity handlePut(HttpServletRequest request) {
39 | return handleTheError(request);
40 | }
41 |
42 | @DeleteMapping(path = "/error",
43 | produces = MediaType.APPLICATION_JSON_VALUE
44 | )
45 | public ResponseEntity handleDelete(HttpServletRequest request) {
46 | return handleTheError(request);
47 | }
48 |
49 | @PatchMapping(path = "/error",
50 | produces = MediaType.APPLICATION_JSON_VALUE
51 | )
52 | public ResponseEntity handlePatch(HttpServletRequest request) {
53 | return handleTheError(request);
54 | }
55 |
56 | private ResponseEntity handleTheError(HttpServletRequest request) {
57 | Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
58 | if (status != null) {
59 | Integer statusCode = Integer.valueOf(status.toString());
60 | log.trace("Handling error: {}", statusCode);
61 | JsonObject jsonObject = new JsonObject();
62 | if(statusCode == HttpStatus.NOT_FOUND.value()) {
63 | jsonObject.put(Constants.ERROR_MESSAGE, "The requested route was not found, please try again later on.");
64 | jsonObject.put(Constants.ERROR_CODE, statusCode);
65 | return new ResponseEntity<>(jsonObject.toJson(), HttpStatus.NOT_FOUND);
66 | } else {
67 | jsonObject.put(Constants.ERROR_MESSAGE, "The requested route is not available, please try again later on.");
68 | jsonObject.put(Constants.ERROR_CODE, HttpStatus.UNAUTHORIZED.value());
69 | request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, HttpStatus.UNAUTHORIZED.value());
70 | return new ResponseEntity<>(jsonObject.toJson(), HttpStatus.UNAUTHORIZED);
71 | }
72 | }
73 | JsonObject jsonObject = new JsonObject();
74 | jsonObject.put(Constants.ERROR_MESSAGE, "The requested route is not available, please try again later on.");
75 | jsonObject.put(Constants.ERROR_CODE, HttpStatus.UNAUTHORIZED.value());
76 | return new ResponseEntity<>(jsonObject.toJson(), HttpStatus.INTERNAL_SERVER_ERROR);
77 | }
78 | }
--------------------------------------------------------------------------------
/src/main/java/io/surisoft/capi/controller/DefinitionController.java:
--------------------------------------------------------------------------------
1 | package io.surisoft.capi.controller;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import io.surisoft.capi.exception.AuthorizationException;
5 | import io.surisoft.capi.schema.Service;
6 | import io.surisoft.capi.utils.HttpUtils;
7 | import jakarta.servlet.http.HttpServletRequest;
8 | import org.apache.camel.util.json.JsonArray;
9 | import org.apache.camel.util.json.JsonObject;
10 | import org.cache2k.Cache;
11 | import org.slf4j.Logger;
12 | import org.slf4j.LoggerFactory;
13 | import org.springframework.beans.factory.annotation.Value;
14 | import org.springframework.http.HttpStatus;
15 | import org.springframework.http.ResponseEntity;
16 | import org.springframework.web.bind.annotation.GetMapping;
17 | import org.springframework.web.bind.annotation.PathVariable;
18 | import org.springframework.web.bind.annotation.RequestMapping;
19 | import org.springframework.web.bind.annotation.RestController;
20 |
21 | import java.io.IOException;
22 | import java.net.URI;
23 | import java.net.http.HttpClient;
24 | import java.net.http.HttpRequest;
25 | import java.net.http.HttpResponse;
26 |
27 | @RestController
28 | @RequestMapping("/definitions/openapi")
29 | public class DefinitionController {
30 |
31 | private static final Logger log = LoggerFactory.getLogger(DefinitionController.class);
32 | private final Cache serviceCache;
33 | private final String capiPublicEndpoint;
34 | private final HttpUtils httpUtils;
35 |
36 | public DefinitionController(Cache serviceCache,
37 | @Value("${capi.public-endpoint}") String capiPublicEndpoint,
38 | HttpUtils httpUtils) {
39 | this.serviceCache = serviceCache;
40 | this.capiPublicEndpoint = capiPublicEndpoint;
41 | this.httpUtils = httpUtils;
42 | }
43 |
44 | @GetMapping(path= "/{serviceId}", produces="application/json")
45 | public ResponseEntity getServiceOpenApi(@PathVariable String serviceId, HttpServletRequest request) {
46 | if(serviceCache.containsKey(serviceId)) {
47 | try {
48 | Service service = serviceCache.get(serviceId);
49 | if(service != null && service.getServiceMeta() != null && service.getServiceMeta().getOpenApiEndpoint() != null) {
50 | if(service.getServiceMeta().isExposeOpenApiDefinition()) {
51 | if(service.getServiceMeta().isSecureOpenApiDefinition()) {
52 | String accessToken = httpUtils.processAuthorizationAccessToken(request);
53 | if(accessToken != null) {
54 | if(httpUtils.isAuthorized(accessToken, service.getServiceMeta().getSubscriptionGroup())) {
55 | JsonObject responseObject = getDefinition(service);
56 | return new ResponseEntity<>(responseObject, HttpStatus.OK);
57 | }
58 | }
59 | } else {
60 | JsonObject responseObject = getDefinition(service);
61 | return new ResponseEntity<>(responseObject, HttpStatus.OK);
62 | }
63 | }
64 | }
65 | return new ResponseEntity<>(HttpStatus.NOT_FOUND);
66 | } catch (NullPointerException | IOException | InterruptedException | AuthorizationException e) {
67 | return new ResponseEntity<>(HttpStatus.NOT_FOUND);
68 | }
69 | }
70 | return new ResponseEntity<>(HttpStatus.NOT_FOUND);
71 | }
72 |
73 | private JsonObject getDefinition(Service service) throws IOException, InterruptedException {
74 | ObjectMapper objectMapper = new ObjectMapper();
75 | HttpClient client = HttpClient.newBuilder().build();
76 | HttpRequest request = HttpRequest.newBuilder().uri(URI.create(service.getServiceMeta().getOpenApiEndpoint())).build();
77 | HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
78 |
79 | JsonObject serverObject = new JsonObject();
80 | serverObject.put("url", capiPublicEndpoint + service.getId().replaceAll(":", "/"));
81 | JsonObject responseObject = objectMapper.readValue(response.body(), JsonObject.class);
82 | responseObject.remove("servers");
83 |
84 | JsonArray serversArray = new JsonArray();
85 | serversArray.add(serverObject);
86 |
87 | JsonObject infoObject = new JsonObject();
88 | infoObject.put("title", service.getId());
89 | infoObject.put("description", "Open API definition generated by CAPI");
90 | responseObject.put("info", infoObject);
91 | responseObject.put("servers", serversArray);
92 | return responseObject;
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/main/java/io/surisoft/capi/controller/ErrorController.java:
--------------------------------------------------------------------------------
1 | package io.surisoft.capi.controller;
2 |
3 | import io.surisoft.capi.schema.CapiRestError;
4 | import io.surisoft.capi.utils.Constants;
5 | import jakarta.servlet.http.HttpServletRequest;
6 | import org.springframework.http.HttpStatus;
7 | import org.springframework.http.MediaType;
8 | import org.springframework.http.ResponseEntity;
9 | import org.springframework.web.bind.annotation.*;
10 |
11 | import java.util.Objects;
12 |
13 | @RestController
14 | public class ErrorController {
15 | @GetMapping(path = Constants.CAPI_INTERNAL_REST_ERROR_PATH + "/**", produces = MediaType.APPLICATION_JSON_VALUE)
16 | public ResponseEntity get(HttpServletRequest request) {
17 | return buildResponse(request);
18 | }
19 |
20 | @PostMapping(path = Constants.CAPI_INTERNAL_REST_ERROR_PATH + "/**", produces = MediaType.APPLICATION_JSON_VALUE)
21 | public ResponseEntity post(HttpServletRequest request) {
22 | return buildResponse(request);
23 | }
24 |
25 | @PutMapping(path = Constants.CAPI_INTERNAL_REST_ERROR_PATH, produces = MediaType.APPLICATION_JSON_VALUE)
26 | public ResponseEntity put(HttpServletRequest request) {
27 | return buildResponse(request);
28 | }
29 |
30 | @DeleteMapping(path = Constants.CAPI_INTERNAL_REST_ERROR_PATH, produces = MediaType.APPLICATION_JSON_VALUE)
31 | public ResponseEntity delete(HttpServletRequest request) {
32 | return buildResponse(request);
33 | }
34 |
35 | private ResponseEntity buildResponse(HttpServletRequest request) {
36 | CapiRestError capiRestError = new CapiRestError();
37 |
38 | String errorMessage = request.getHeader(Constants.REASON_MESSAGE_HEADER);
39 |
40 | if(request.getHeader(Constants.ROUTE_ID_HEADER) != null) {
41 | capiRestError.setRouteID(request.getHeader(Constants.ROUTE_ID_HEADER));
42 | }
43 |
44 | if(request.getHeader(Constants.CAPI_URI_IN_ERROR) != null) {
45 | capiRestError.setHttpUri(request.getHeader(Constants.CAPI_URI_IN_ERROR));
46 | }
47 |
48 | if(Boolean.parseBoolean(request.getHeader(Constants.ERROR_API_SHOW_TRACE_ID))) {
49 | capiRestError.setTraceID(request.getHeader(Constants.TRACE_ID_HEADER));
50 | }
51 |
52 | capiRestError.setErrorMessage(Objects.requireNonNullElse(errorMessage, "There was an exception connecting to the requested service, please try again later on."));
53 |
54 | if(request.getHeader(Constants.TRACE_ID_HEADER) != null) {
55 | capiRestError.setTraceID(request.getHeader(Constants.TRACE_ID_HEADER));
56 | }
57 |
58 | if(request.getHeader(Constants.REASON_CODE_HEADER) != null) {
59 | int returnedCode = Integer.parseInt(request.getHeader(Constants.REASON_CODE_HEADER));
60 | capiRestError.setErrorCode(returnedCode);
61 | } else {
62 | capiRestError.setErrorCode(HttpStatus.BAD_GATEWAY.value());
63 | }
64 | return new ResponseEntity<>(capiRestError, HttpStatus.valueOf(capiRestError.getErrorCode()));
65 | }
66 | }
--------------------------------------------------------------------------------
/src/main/java/io/surisoft/capi/controller/PublicHealthController.java:
--------------------------------------------------------------------------------
1 | package io.surisoft.capi.controller;
2 |
3 | import io.surisoft.capi.service.ConsulNodeDiscovery;
4 | import org.springframework.http.HttpStatus;
5 | import org.springframework.http.ResponseEntity;
6 | import org.springframework.web.bind.annotation.GetMapping;
7 | import org.springframework.web.bind.annotation.RequestMapping;
8 | import org.springframework.web.bind.annotation.RestController;
9 |
10 | @RestController
11 | @RequestMapping("/health")
12 | public class PublicHealthController {
13 | @GetMapping
14 | public ResponseEntity amIHealthy() {
15 | if(ConsulNodeDiscovery.isConnectedToConsul()) {
16 | return new ResponseEntity<>(HttpStatus.OK);
17 | }
18 | return new ResponseEntity<>(HttpStatus.SERVICE_UNAVAILABLE);
19 | }
20 | }
--------------------------------------------------------------------------------
/src/main/java/io/surisoft/capi/exception/AuthorizationException.java:
--------------------------------------------------------------------------------
1 | package io.surisoft.capi.exception;
2 |
3 | public class AuthorizationException extends Exception {
4 | public AuthorizationException(String message) {
5 | super(message);
6 | }
7 | }
--------------------------------------------------------------------------------
/src/main/java/io/surisoft/capi/exception/CapiUndertowException.java:
--------------------------------------------------------------------------------
1 | package io.surisoft.capi.exception;
2 |
3 | public class CapiUndertowException extends Exception {
4 | public CapiUndertowException(String message) {
5 | super(message);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/java/io/surisoft/capi/exception/RestTemplateErrorHandler.java:
--------------------------------------------------------------------------------
1 | package io.surisoft.capi.exception;
2 |
3 | import org.jetbrains.annotations.NotNull;
4 | import org.springframework.http.client.ClientHttpResponse;
5 | import org.springframework.web.client.ResponseErrorHandler;
6 |
7 | import java.io.IOException;
8 |
9 | public class RestTemplateErrorHandler implements ResponseErrorHandler {
10 | @Override
11 | public boolean hasError(ClientHttpResponse response) throws IOException {
12 | return response.getStatusCode().isError();
13 | }
14 |
15 | @Override
16 | public void handleError(@NotNull ClientHttpResponse response) {
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/java/io/surisoft/capi/kafka/CapiEventSerializer.java:
--------------------------------------------------------------------------------
1 | package io.surisoft.capi.kafka;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import io.surisoft.capi.schema.CapiEvent;
5 | import org.apache.kafka.common.errors.SerializationException;
6 | import org.apache.kafka.common.serialization.Serializer;
7 | import org.slf4j.Logger;
8 | import org.slf4j.LoggerFactory;
9 |
10 | public class CapiEventSerializer implements Serializer {
11 | private static final Logger log = LoggerFactory.getLogger(CapiEventSerializer.class);
12 | private final ObjectMapper objectMapper = new ObjectMapper();
13 |
14 | @Override
15 | public byte[] serialize(String topic, CapiEvent data) {
16 | try {
17 | if (data == null){
18 | log.warn("Null received at serializing");
19 | return null;
20 | }
21 | return objectMapper.writeValueAsBytes(data);
22 | } catch (Exception e) {
23 | throw new SerializationException("Error when serializing MessageDto to byte[]");
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/src/main/java/io/surisoft/capi/kafka/CapiInstance.java:
--------------------------------------------------------------------------------
1 | package io.surisoft.capi.kafka;
2 |
3 | public record CapiInstance(String uuid) {
4 | }
5 |
--------------------------------------------------------------------------------
/src/main/java/io/surisoft/capi/kafka/CapiKafkaEvent.java:
--------------------------------------------------------------------------------
1 | package io.surisoft.capi.kafka;
2 |
3 | import io.surisoft.capi.cache.StickySessionCacheManager;
4 | import io.surisoft.capi.schema.CapiEvent;
5 | import io.surisoft.capi.schema.StickySession;
6 | import io.surisoft.capi.schema.ThrottleServiceObject;
7 | import org.cache2k.Cache;
8 | import org.slf4j.Logger;
9 | import org.slf4j.LoggerFactory;
10 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
11 | import org.springframework.context.annotation.Bean;
12 | import org.springframework.context.annotation.Configuration;
13 |
14 | @Configuration
15 | @ConditionalOnProperty(prefix = "capi.kafka", name = "enabled", havingValue = "true")
16 | public class CapiKafkaEvent {
17 |
18 | public static final String STICKY_SESSION_EVENT_TYPE = "sticky-session";
19 | public static final String THROTTLING_EVENT_TYPE = "throttling";
20 |
21 | private final CapiInstance capiInstance;
22 | private final StickySessionCacheManager stickySessionCacheManager;
23 | private final Cache throttleServiceObjectCache;
24 |
25 | public CapiKafkaEvent(CapiInstance capiInstance,
26 | StickySessionCacheManager stickySessionCacheManager,
27 | Cache throttleServiceObjectCache) {
28 | this.capiInstance = capiInstance;
29 | this.stickySessionCacheManager = stickySessionCacheManager;
30 | this.throttleServiceObjectCache = throttleServiceObjectCache;
31 |
32 | log.trace("CAPI Instance ID: {}", capiInstance.uuid());
33 |
34 | }
35 |
36 | private static final Logger log = LoggerFactory.getLogger(CapiKafkaEvent.class);
37 | public void process(CapiEvent incomingEvent) {
38 | //log.trace(capiInstance.uuid());
39 | if(incomingEvent.getInstanceId().equals(capiInstance.uuid())) {
40 | log.trace("Event {} is from this instance, ignoring", incomingEvent.getId());
41 | return;
42 | }
43 | if(incomingEvent.getType().equals(STICKY_SESSION_EVENT_TYPE)) {
44 | StickySession stickySession = new StickySession();
45 | stickySession.setParamValue(incomingEvent.getValue());
46 | stickySession.setParamName(incomingEvent.getKey());
47 | stickySession.setNodeIndex(incomingEvent.getNodeIndex());
48 | stickySessionCacheManager.createStickySession(stickySession, false);
49 | log.trace("Event {} is a sticky session event", incomingEvent.getId());
50 | return;
51 | }
52 | if(incomingEvent.getType().equals(THROTTLING_EVENT_TYPE) && incomingEvent.getThrottleServiceObject() != null) {
53 | log.trace("Event {} is a throttling service object", incomingEvent.getKey());
54 |
55 | log.trace("Incoming Object");
56 | log.trace(incomingEvent.getThrottleServiceObject().getCacheKey());
57 | log.trace("{}", incomingEvent.getThrottleServiceObject().getTotalCallsAllowed());
58 | log.trace("{}", incomingEvent.getThrottleServiceObject().getCurrentCalls());
59 | log.trace("{}", incomingEvent.getThrottleServiceObject().getExpirationTime());
60 |
61 | throttleServiceObjectCache.put(incomingEvent.getKey(), incomingEvent.getThrottleServiceObject());
62 | return;
63 | }
64 | log.trace("Received event: {}", incomingEvent);
65 | }
66 |
67 | @Bean(name = "capiKafkaEventProcessor")
68 | public CapiKafkaEvent capiKafkaEvent() {
69 | return this;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/main/java/io/surisoft/capi/kafka/CapiKafkaEventDeserializer.java:
--------------------------------------------------------------------------------
1 | package io.surisoft.capi.kafka;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import io.surisoft.capi.schema.CapiEvent;
5 | import org.apache.kafka.common.errors.SerializationException;
6 | import org.apache.kafka.common.header.Headers;
7 | import org.apache.kafka.common.serialization.Deserializer;
8 | import org.slf4j.Logger;
9 | import org.slf4j.LoggerFactory;
10 |
11 | import java.nio.charset.StandardCharsets;
12 | import java.util.Map;
13 |
14 | public class CapiKafkaEventDeserializer implements Deserializer {
15 | private static final Logger log = LoggerFactory.getLogger(CapiKafkaEventDeserializer.class);
16 | private final ObjectMapper objectMapper = new ObjectMapper();
17 | @Override
18 | public void configure(Map configs, boolean isKey) {
19 | Deserializer.super.configure(configs, isKey);
20 | }
21 |
22 | @Override
23 | public CapiEvent deserialize(String topic, byte[] data) {
24 | try {
25 | if (data == null){
26 | log.warn("Null received at deserializing");
27 | return null;
28 | }
29 | return objectMapper.readValue(new String(data, StandardCharsets.UTF_8), CapiEvent.class);
30 | } catch (Exception e) {
31 | throw new SerializationException("Error when deserializing byte[] to MessageDto");
32 | }
33 | }
34 |
35 | @Override
36 | public CapiEvent deserialize(String topic, Headers headers, byte[] data) {
37 | return Deserializer.super.deserialize(topic, headers, data);
38 | }
39 |
40 | @Override
41 | public void close() {
42 | Deserializer.super.close();
43 | }
44 | }
--------------------------------------------------------------------------------
/src/main/java/io/surisoft/capi/metrics/HealthController.java:
--------------------------------------------------------------------------------
1 | package io.surisoft.capi.metrics;
2 |
3 | import io.surisoft.capi.service.ConsulNodeDiscovery;
4 | import org.springframework.boot.actuate.health.Health;
5 | import org.springframework.boot.actuate.health.HealthIndicator;
6 | import org.springframework.stereotype.Component;
7 |
8 | @Component
9 | public class HealthController implements HealthIndicator {
10 | @Override
11 | public Health health() {
12 | if(ConsulNodeDiscovery.isConnectedToConsul()) {
13 | return Health.up().build();
14 | }
15 | return Health.down().withDetail("reason", "Consul not available").build();
16 | }
17 | }
--------------------------------------------------------------------------------
/src/main/java/io/surisoft/capi/metrics/Info.java:
--------------------------------------------------------------------------------
1 | package io.surisoft.capi.metrics;
2 |
3 | import io.surisoft.capi.schema.CapiInfo;
4 | import org.apache.camel.CamelContext;
5 | import org.springframework.beans.factory.annotation.Value;
6 | import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
7 | import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
8 | import org.springframework.stereotype.Component;
9 |
10 | import java.util.List;
11 | import java.util.stream.Collectors;
12 |
13 | @Component
14 | @Endpoint(id = "capi")
15 | public class Info {
16 |
17 | private final List oauth2Keys;
18 | private final CamelContext camelContext;
19 |
20 | public Info(List oauth2Keys, CamelContext camelContext) {
21 | this.oauth2Keys = oauth2Keys;
22 | this.camelContext = camelContext;
23 | }
24 |
25 | @Value("${capi.version}")
26 | private String capiVersion;
27 | @Value("${capi.namespace}")
28 | private String capiNameSpace;
29 | @Value("${capi.spring.version}")
30 | private String capiSpringVersion;
31 | @Value("${capi.oauth2.provider.enabled}")
32 | private boolean oauth2Enabled;
33 |
34 | @Value("${capi.opa.enabled}")
35 | private boolean opaEnabled;
36 | @Value("${capi.opa.endpoint}")
37 | private String opaEndpoint;
38 | @Value("${capi.consul.discovery.enabled}")
39 | private boolean consulEnabled;
40 | @Value("${capi.consul.hosts}")
41 | private String consulEndpoint;
42 | @Value("${capi.consul.discovery.timer.interval}")
43 | private int consulTimerInterval;
44 | @Value("${camel.servlet.mapping.context-path}")
45 | private String routeContextPath;
46 | @Value("${management.endpoints.web.base-path}")
47 | private String metricsContextPath;
48 | @Value("${capi.traces.enabled}")
49 | private boolean tracesEnabled;
50 | @Value("${capi.traces.endpoint}")
51 | private String tracesEndpoint;
52 |
53 |
54 | @ReadOperation
55 | public CapiInfo getInfo() {
56 | CapiInfo capiInfo = new CapiInfo();
57 | capiInfo.setUptime(camelContext.getUptime().toString());
58 | capiInfo.setCamelVersion(camelContext.getVersion());
59 | capiInfo.setTotalRoutes(camelContext.getRoutesSize());
60 | capiInfo.setCapiVersion(capiVersion);
61 | capiInfo.setCapiStringVersion(capiSpringVersion);
62 | capiInfo.setCapiNameSpace(capiNameSpace);
63 | capiInfo.setOpaEnabled(opaEnabled);
64 | capiInfo.setOpaEndpoint(opaEndpoint);
65 | capiInfo.setOauth2Enabled(oauth2Enabled);
66 | capiInfo.setOauth2Endpoint(oauth2Keys.stream().map(String::valueOf).collect(Collectors.joining(",")));
67 | capiInfo.setConsulEnabled(consulEnabled);
68 | capiInfo.setConsulEndpoint(consulEndpoint);
69 | capiInfo.setConsulTimerInterval(consulTimerInterval);
70 | capiInfo.setRoutesContextPath(routeContextPath);
71 | capiInfo.setMetricsContextPath(metricsContextPath);
72 | capiInfo.setTracesEnabled(tracesEnabled);
73 | capiInfo.setTracesEndpoint(tracesEndpoint);
74 | capiInfo.setJavaVersion(String.valueOf(Runtime.version()));
75 | return capiInfo;
76 | }
77 | }
--------------------------------------------------------------------------------
/src/main/java/io/surisoft/capi/metrics/KVStore.java:
--------------------------------------------------------------------------------
1 | package io.surisoft.capi.metrics;
2 |
3 | import org.cache2k.Cache;
4 | import org.cache2k.CacheEntry;
5 | import org.springframework.boot.actuate.endpoint.annotation.*;
6 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
7 | import org.springframework.stereotype.Component;
8 |
9 | import java.util.ArrayList;
10 | import java.util.List;
11 | import java.util.Objects;
12 | import java.util.Set;
13 |
14 | @Component
15 | @ConditionalOnProperty(prefix = "capi.consul.kv", name = "enabled", havingValue = "true")
16 | @Endpoint(id = "kv")
17 | public class KVStore {
18 |
19 | private final Cache> corsHeadersCache;
20 |
21 | public KVStore(Cache> corsHeadersCache) {
22 | this.corsHeadersCache = corsHeadersCache;
23 | }
24 |
25 |
26 | @ReadOperation
27 | public Set>> getCache() {
28 | return corsHeadersCache.entries();
29 | }
30 |
31 | @WriteOperation
32 | public List addHeader(@Selector String key, @Selector String value) {
33 | List headersCached = new ArrayList<>(Objects.requireNonNull(corsHeadersCache.get(key)));
34 | if(!headersCached.isEmpty()) {
35 | headersCached.add(value);
36 | corsHeadersCache.put(key, headersCached);
37 | }
38 | return headersCached;
39 | }
40 |
41 | @DeleteOperation
42 | public List deleteHeader(@Selector String key, @Selector String value) {
43 | List headersToKeep = new ArrayList<>();
44 | for(String header : Objects.requireNonNull(corsHeadersCache.get(key))) {
45 | if(!header.equals(value)) {
46 | headersToKeep.add(header);
47 | }
48 | corsHeadersCache.put(key, headersToKeep);
49 | }
50 | return headersToKeep;
51 | }
52 | }
--------------------------------------------------------------------------------
/src/main/java/io/surisoft/capi/metrics/OpenAPIDefinition.java:
--------------------------------------------------------------------------------
1 | package io.surisoft.capi.metrics;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import io.surisoft.capi.schema.Service;
5 | import org.apache.camel.util.json.JsonArray;
6 | import org.apache.camel.util.json.JsonObject;
7 | import org.cache2k.Cache;
8 | import org.springframework.beans.factory.annotation.Value;
9 | import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
10 | import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
11 | import org.springframework.boot.actuate.endpoint.annotation.Selector;
12 | import org.springframework.stereotype.Component;
13 |
14 | import java.io.IOException;
15 | import java.net.URI;
16 | import java.net.http.HttpClient;
17 | import java.net.http.HttpRequest;
18 | import java.net.http.HttpResponse;
19 |
20 | @Component
21 | @Endpoint(id = "openapi")
22 | public class OpenAPIDefinition {
23 |
24 | private final Cache serviceCache;
25 |
26 | @Value("${capi.public-endpoint}")
27 | private String capiPublicEndpoint;
28 |
29 | public OpenAPIDefinition(Cache serviceCache) {
30 | this.serviceCache = serviceCache;
31 | }
32 |
33 | @ReadOperation
34 | public JsonObject getCacheOpenApiDefinition(@Selector String serviceName) {
35 | if(serviceCache.containsKey(serviceName)) {
36 | ObjectMapper objectMapper = new ObjectMapper();
37 | try {
38 | Service service = serviceCache.get(serviceName);
39 | if(service != null && service.getServiceMeta() != null && service.getServiceMeta().getOpenApiEndpoint() != null) {
40 | HttpClient client = HttpClient.newBuilder().build();
41 | HttpRequest request = HttpRequest.newBuilder().uri(URI.create(service.getServiceMeta().getOpenApiEndpoint())).build();
42 | HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
43 | JsonObject serverObject = new JsonObject();
44 | serverObject.put("url", capiPublicEndpoint + serviceName.replaceAll(":", "/"));
45 | JsonObject responseObject = objectMapper.readValue(response.body(), JsonObject.class);
46 | responseObject.remove("servers");
47 |
48 | JsonArray serversArray = new JsonArray();
49 | serversArray.add(serverObject);
50 |
51 | JsonObject infoObject = new JsonObject();
52 | infoObject.put("title", service.getId());
53 | infoObject.put("description", "Open API definition generated by CAPI");
54 | responseObject.put("info", infoObject);
55 | responseObject.put("servers", serversArray);
56 | return responseObject;
57 | }
58 | return null;
59 | } catch (NullPointerException | IOException | InterruptedException e) {
60 | return null;
61 | }
62 | }
63 | return null;
64 | }
65 | }
--------------------------------------------------------------------------------
/src/main/java/io/surisoft/capi/metrics/Routes.java:
--------------------------------------------------------------------------------
1 | package io.surisoft.capi.metrics;
2 |
3 | import io.surisoft.capi.processor.MetricsProcessor;
4 | import io.surisoft.capi.schema.RouteDetailsEndpointInfo;
5 | import io.surisoft.capi.schema.RouteEndpointInfo;
6 | import io.surisoft.capi.schema.Service;
7 | import io.surisoft.capi.schema.StickySession;
8 | import io.surisoft.capi.utils.Constants;
9 | import io.surisoft.capi.utils.RouteUtils;
10 | import io.surisoft.capi.utils.ServiceUtils;
11 | import org.apache.camel.CamelContext;
12 | import org.cache2k.Cache;
13 | import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
14 | import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
15 | import org.springframework.boot.actuate.endpoint.annotation.Selector;
16 | import org.springframework.stereotype.Component;
17 |
18 | import java.util.ArrayList;
19 | import java.util.List;
20 |
21 | @Component
22 | @Endpoint(id = "routes")
23 | public class Routes {
24 |
25 | private final ServiceUtils serviceUtils;
26 | private final Cache serviceCache;
27 | private final Cache stickySessionCache;
28 | private final CamelContext camelContext;
29 | private final RouteUtils routeUtils;
30 | private final MetricsProcessor metricsProcessor;
31 |
32 | public Routes(ServiceUtils serviceUtils,
33 | Cache serviceCache,
34 | Cache stickySessionCache,
35 | CamelContext camelContext,
36 | RouteUtils routeUtils,
37 | MetricsProcessor metricsProcessor) {
38 | this.serviceUtils = serviceUtils;
39 | this.serviceCache = serviceCache;
40 | this.stickySessionCache = stickySessionCache;
41 | this.camelContext = camelContext;
42 | this.routeUtils = routeUtils;
43 | this.metricsProcessor = metricsProcessor;
44 | }
45 |
46 | @ReadOperation
47 | public Service getCachedService(@Selector String serviceName) {
48 | if(serviceCache.containsKey(serviceName)) {
49 | return serviceCache.get(serviceName);
50 | }
51 | return null;
52 | }
53 |
54 | @ReadOperation
55 | public List getAllRoutesInfo() {
56 | List detailInfoList = new ArrayList<>();
57 | List routeEndpointInfoList = camelContext.getRoutes().stream()
58 | .map(RouteEndpointInfo::new)
59 | .toList();
60 | for(RouteEndpointInfo routeEndpointInfo : routeEndpointInfoList) {
61 | if(!Constants.CAPI_INTERNAL_ROUTES_PREFIX.contains(routeEndpointInfo.getId())) {
62 | detailInfoList.add(new RouteDetailsEndpointInfo(camelContext, camelContext.getRoute(routeEndpointInfo.getId())));
63 | }
64 | }
65 | return detailInfoList;
66 | }
67 | }
--------------------------------------------------------------------------------
/src/main/java/io/surisoft/capi/metrics/WSRoutes.java:
--------------------------------------------------------------------------------
1 | package io.surisoft.capi.metrics;
2 |
3 | import io.surisoft.capi.schema.WebsocketClient;
4 | import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
5 | import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
6 | import org.springframework.stereotype.Component;
7 |
8 | import java.util.Map;
9 | import java.util.Optional;
10 |
11 | @Component
12 | @Endpoint(id = "ws-routes")
13 | public class WSRoutes {
14 |
15 | private final Optional