Compare commits
	
		
			22 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 4075eb78c8 | |||
| 68792e2cbf | |||
| 316dd6b01e | |||
| a1275ec06c | |||
| 6794e56748 | |||
| 25ef5660fa | |||
| 0d576eb9a7 | |||
| f99ed01a54 | |||
| 18e3535a57 | |||
| 4d63eb2e2c | |||
| e9e6bd69f9 | |||
| c8e8817e25 | |||
| 057d0120b7 | |||
| b804cc978f | |||
| 729d0ddcfc | |||
| bbadd41ec4 | |||
| c567cf766d | |||
| fb4ff60729 | |||
| 6b6ef23108 | |||
| 0f04461a92 | |||
| b98dad9c4b | |||
| 831aaa41eb | 
| @ -34,13 +34,6 @@ jobs: | |||||||
|         uses: actions/checkout@v4 |         uses: actions/checkout@v4 | ||||||
|         with: |         with: | ||||||
|           fetch-depth: 0 |           fetch-depth: 0 | ||||||
|       - name: JDK setup |  | ||||||
|         uses: actions/setup-java@v4 |  | ||||||
|         with: |  | ||||||
|           distribution: 'zulu' |  | ||||||
|           java-version: '17' |  | ||||||
|       - name: Validate Gradle Wrapper |  | ||||||
|         uses: gradle/actions/wrapper-validation@v3 |  | ||||||
|       - name: Create and push tag |       - name: Create and push tag | ||||||
|         run: | |         run: | | ||||||
|           echo "NEW_VERSION=${{ needs.tag.outputs.new_version }}" |           echo "NEW_VERSION=${{ needs.tag.outputs.new_version }}" | ||||||
| @ -54,10 +47,10 @@ jobs: | |||||||
|           registry: 'http://192.168.0.100:8928' |           registry: 'http://192.168.0.100:8928' | ||||||
|           username: hitanshu |           username: hitanshu | ||||||
|           password: ${{ secrets.TOKEN }} |           password: ${{ secrets.TOKEN }} | ||||||
|       - name: Gradle build |       - name: Tag prod image | ||||||
|         run: ./gradlew bootBuildImage --imageName=192.168.0.100:8928/hithomelabs/cftunnels:${{ needs.tag.outputs.new_version }} |         run: | | ||||||
|       - name: Tag image as test |           docker tag 192.168.0.100:8928/hithomelabs/cftunnels:test 192.168.0.100:8928/hithomelabs/cftunnels:${{ needs.tag.outputs.new_version }} | ||||||
|         run: docker tag 192.168.0.100:8928/hithomelabs/cftunnels:${{ needs.tag.outputs.new_version }} 192.168.0.100:8928/hithomelabs/cftunnels:prod |           docker tag 192.168.0.100:8928/hithomelabs/cftunnels:${{ needs.tag.outputs.new_version }} 192.168.0.100:8928/hithomelabs/cftunnels:prod | ||||||
|       - name: Push to Gitea Registry |       - name: Push to Gitea Registry | ||||||
|         run: | |         run: | | ||||||
|           docker push 192.168.0.100:8928/hithomelabs/cftunnels:prod |           docker push 192.168.0.100:8928/hithomelabs/cftunnels:prod | ||||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -1,6 +1,8 @@ | |||||||
| HELP.md | HELP.md | ||||||
| .gradle | .gradle | ||||||
|  | .run | ||||||
| build/ | build/ | ||||||
|  | .env* | ||||||
| !gradle/wrapper/gradle-wrapper.jar | !gradle/wrapper/gradle-wrapper.jar | ||||||
| !**/src/main/**/build/ | !**/src/main/**/build/ | ||||||
| !**/src/test/**/build/ | !**/src/test/**/build/ | ||||||
|  | |||||||
| @ -24,9 +24,15 @@ repositories { | |||||||
| dependencies { | dependencies { | ||||||
| 	implementation group: 'org.springdoc', name: 'springdoc-openapi-starter-webmvc-ui', version: '2.8.5' | 	implementation group: 'org.springdoc', name: 'springdoc-openapi-starter-webmvc-ui', version: '2.8.5' | ||||||
| 	implementation group: 'org.springframework.boot', name:'spring-boot-starter-oauth2-client', version: '3.5.5' | 	implementation group: 'org.springframework.boot', name:'spring-boot-starter-oauth2-client', version: '3.5.5' | ||||||
|  | 	compileOnly 'org.projectlombok:lombok:1.18.30' | ||||||
|  | 	annotationProcessor 'org.projectlombok:lombok:1.18.30' | ||||||
| 	implementation 'org.springframework.boot:spring-boot-starter-web' | 	implementation 'org.springframework.boot:spring-boot-starter-web' | ||||||
| 	testImplementation 'org.springframework.boot:spring-boot-starter-test' | 	testImplementation 'org.springframework.boot:spring-boot-starter-test' | ||||||
|  | 	testImplementation 'org.springframework.security:spring-security-test' | ||||||
| 	testRuntimeOnly 'org.junit.platform:junit-platform-launcher' | 	testRuntimeOnly 'org.junit.platform:junit-platform-launcher' | ||||||
|  | 	implementation 'org.springframework.boot:spring-boot-starter-data-jpa' | ||||||
|  | 	runtimeOnly 'org.postgresql:postgresql' | ||||||
|  | 	implementation 'org.hibernate.validator:hibernate-validator' | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| tasks.named('test') { | tasks.named('test') { | ||||||
|  | |||||||
| @ -2,6 +2,8 @@ services: | |||||||
|   app: |   app: | ||||||
|     image: gitea.hithomelabs.com/hithomelabs/cftunnels:${ENV} |     image: gitea.hithomelabs.com/hithomelabs/cftunnels:${ENV} | ||||||
|     container_name: cftunnels_${ENV} |     container_name: cftunnels_${ENV} | ||||||
|  |     ports: | ||||||
|  |       - ${HOST_PORT}:8080 | ||||||
|     environment: |     environment: | ||||||
|       - CLOUDFLARE_ACCOUNT_ID=${CLOUDFLARE_ACCOUNT_ID} |       - CLOUDFLARE_ACCOUNT_ID=${CLOUDFLARE_ACCOUNT_ID} | ||||||
|       - CLOUDFLARE_API_KEY=${CLOUDFLARE_API_KEY} |       - CLOUDFLARE_API_KEY=${CLOUDFLARE_API_KEY} | ||||||
| @ -9,6 +11,21 @@ services: | |||||||
|       - ENV=${ENV} |       - ENV=${ENV} | ||||||
|       - OAUTH_CLIENT_ID=${OAUTH_CLIENT_ID} |       - OAUTH_CLIENT_ID=${OAUTH_CLIENT_ID} | ||||||
|       - OAUTH_CLIENT_SECRET=${OAUTH_CLIENT_SECRET} |       - OAUTH_CLIENT_SECRET=${OAUTH_CLIENT_SECRET} | ||||||
|  |       - HOST_PORT=${HOST_PORT} | ||||||
|  |       - POSTGRES_USER=${POSTGRES_USERNAME} | ||||||
|  |       - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} | ||||||
|  |     env_file: | ||||||
|  |       - stack.env | ||||||
|  |     restart: unless-stopped | ||||||
|  |   postgres: | ||||||
|  |     image: postgres:15-alpine | ||||||
|  |     container_name: cftunnel-db-${ENV} | ||||||
|  |     environment: | ||||||
|  |       POSTGRES_DB: cftunnel | ||||||
|  |       POSTGRES_USER: ${POSTGRES_USERNAME} | ||||||
|  |       POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} | ||||||
|  |     restart: unless-stopped | ||||||
|     ports: |     ports: | ||||||
|       - 5002:8080 |       - "${DB_PORT}:5432" | ||||||
|     restart: unless-stopped |     volumes: | ||||||
|  |       - ${DB_PATH}:/var/lib/postgresql/data | ||||||
| @ -11,12 +11,12 @@ import java.util.ArrayList; | |||||||
| @Configuration | @Configuration | ||||||
| public class OpenApiConfig { | public class OpenApiConfig { | ||||||
| 
 | 
 | ||||||
|     @Value("${api.corsResolveUrl}") |     @Value("${api.baseUrl}") | ||||||
|     private String corsResolveUrl; |     private String baseUrl; | ||||||
| 
 | 
 | ||||||
|     @Bean |     @Bean | ||||||
|     public OpenAPI openAPI(){ |     public OpenAPI openAPI(){ | ||||||
|         Server httpsServer = new Server().url(corsResolveUrl); |         Server httpsServer = new Server().url(baseUrl); | ||||||
|         OpenAPI openApi = new OpenAPI(); |         OpenAPI openApi = new OpenAPI(); | ||||||
|         ArrayList<Server> servers = new ArrayList<>(); |         ArrayList<Server> servers = new ArrayList<>(); | ||||||
|         servers.add(httpsServer); |         servers.add(httpsServer); | ||||||
|  | |||||||
| @ -16,8 +16,8 @@ import org.springframework.security.web.SecurityFilterChain; | |||||||
| @EnableWebSecurity | @EnableWebSecurity | ||||||
| @EnableMethodSecurity( | @EnableMethodSecurity( | ||||||
|         prePostEnabled = true, |         prePostEnabled = true, | ||||||
|         securedEnabled   = true, |         securedEnabled = true, | ||||||
|         jsr250Enabled    = true |         jsr250Enabled = true | ||||||
| ) | ) | ||||||
| public class SecuirtyConfig { | public class SecuirtyConfig { | ||||||
| 
 | 
 | ||||||
| @ -29,8 +29,9 @@ public class SecuirtyConfig { | |||||||
|         http |         http | ||||||
|                 .authorizeHttpRequests(auth -> auth |                 .authorizeHttpRequests(auth -> auth | ||||||
|                         .anyRequest().authenticated() |                         .anyRequest().authenticated() | ||||||
|                 ) |                 ).csrf(csrf -> csrf.disable()) | ||||||
|                 .with(new OAuth2LoginConfigurer<>(), oauth2 -> oauth2.userInfoEndpoint(u -> u.oidcUserService(customOidcUserConfiguration))); |                 .with(new OAuth2LoginConfigurer<>(), | ||||||
|  |                         oauth2 -> oauth2.userInfoEndpoint(u -> u.oidcUserService(customOidcUserConfiguration))); | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|         return http.build(); |         return http.build(); | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ import com.hithomelabs.CFTunnels.Headers.AuthKeyEmailHeader; | |||||||
| import com.hithomelabs.CFTunnels.Models.Config; | import com.hithomelabs.CFTunnels.Models.Config; | ||||||
| import com.hithomelabs.CFTunnels.Models.Ingress; | import com.hithomelabs.CFTunnels.Models.Ingress; | ||||||
| import com.hithomelabs.CFTunnels.Models.TunnelResponse; | import com.hithomelabs.CFTunnels.Models.TunnelResponse; | ||||||
|  | import com.hithomelabs.CFTunnels.Services.CloudflareAPIService; | ||||||
| import org.springframework.beans.factory.annotation.Autowired; | import org.springframework.beans.factory.annotation.Autowired; | ||||||
| import org.springframework.boot.web.servlet.error.ErrorController; | import org.springframework.boot.web.servlet.error.ErrorController; | ||||||
| import org.springframework.http.*; | import org.springframework.http.*; | ||||||
| @ -40,6 +41,9 @@ public class TunnelController implements ErrorController { | |||||||
|     @Autowired |     @Autowired | ||||||
|     private RestTemplateConfig restTemplateConfig; |     private RestTemplateConfig restTemplateConfig; | ||||||
| 
 | 
 | ||||||
|  |     @Autowired | ||||||
|  |     CloudflareAPIService cloudflareAPIService; | ||||||
|  | 
 | ||||||
|     @PreAuthorize("hasAnyRole('USER')") |     @PreAuthorize("hasAnyRole('USER')") | ||||||
|     @GetMapping("/whoami") |     @GetMapping("/whoami") | ||||||
|     public Map<String,Object> whoAmI(@AuthenticationPrincipal OidcUser oidcUser) { |     public Map<String,Object> whoAmI(@AuthenticationPrincipal OidcUser oidcUser) { | ||||||
| @ -57,12 +61,7 @@ public class TunnelController implements ErrorController { | |||||||
|     @GetMapping("/tunnels") |     @GetMapping("/tunnels") | ||||||
|     public ResponseEntity<Map<String,Object>> getTunnels(){ |     public ResponseEntity<Map<String,Object>> getTunnels(){ | ||||||
| 
 | 
 | ||||||
|         // * * Resource URL to hit get request at |         ResponseEntity<Map> responseEntity = cloudflareAPIService.getCloudflareTunnels(); | ||||||
|         String url = "https://api.cloudflare.com/client/v4/accounts/" + cloudflareConfig.getAccountId() + "/cfd_tunnel"; |  | ||||||
| 
 |  | ||||||
|         HttpEntity<String> httpEntity = new HttpEntity<>("",authKeyEmailHeader.getHttpHeaders()); |  | ||||||
|         ResponseEntity<Map> responseEntity = restTemplate.exchange(url, HttpMethod.GET, httpEntity, Map.class); |  | ||||||
| 
 |  | ||||||
|         Map<String, Object> jsonResponse = new HashMap<>(); |         Map<String, Object> jsonResponse = new HashMap<>(); | ||||||
|         jsonResponse.put("status", "success"); |         jsonResponse.put("status", "success"); | ||||||
|         jsonResponse.put("data", responseEntity.getBody()); |         jsonResponse.put("data", responseEntity.getBody()); | ||||||
| @ -72,14 +71,9 @@ public class TunnelController implements ErrorController { | |||||||
| 
 | 
 | ||||||
|     @PreAuthorize("hasAnyRole('DEVELOPER')") |     @PreAuthorize("hasAnyRole('DEVELOPER')") | ||||||
|     @GetMapping("/tunnel/{tunnelId}") |     @GetMapping("/tunnel/{tunnelId}") | ||||||
|     public ResponseEntity<Map<String,Object>> getTunnelConfigurations(@PathVariable String tunnelId) throws JsonProcessingException { |     public ResponseEntity<Map<String,Object>> getTunnelConfigurations(@PathVariable String tunnelId) { | ||||||
| 
 |  | ||||||
|         // * * Resource URL to hit get request at |  | ||||||
|         String url = "https://api.cloudflare.com/client/v4/accounts/" + cloudflareConfig.getAccountId() + "/cfd_tunnel/" + tunnelId + "/configurations"; |  | ||||||
| 
 |  | ||||||
|         HttpEntity<String> httpEntity = new HttpEntity<>("",authKeyEmailHeader.getHttpHeaders()); |  | ||||||
|         ResponseEntity<Map> responseEntity = restTemplate.exchange(url, HttpMethod.GET, httpEntity, Map.class); |  | ||||||
| 
 | 
 | ||||||
|  |         ResponseEntity<Map> responseEntity = cloudflareAPIService.getCloudflareTunnelConfigurations(tunnelId, restTemplate, Map.class); | ||||||
|         Map<String, Object> jsonResponse = new HashMap<>(); |         Map<String, Object> jsonResponse = new HashMap<>(); | ||||||
|         jsonResponse.put("status", "success"); |         jsonResponse.put("status", "success"); | ||||||
|         jsonResponse.put("data", responseEntity.getBody()); |         jsonResponse.put("data", responseEntity.getBody()); | ||||||
| @ -87,17 +81,12 @@ public class TunnelController implements ErrorController { | |||||||
|         return ResponseEntity.ok(jsonResponse); |         return ResponseEntity.ok(jsonResponse); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| // 50df9101-f625-4618-b7c5-100338a57124 |     // 50df9101-f625-4618-b7c5-100338a57124 | ||||||
|     @PreAuthorize("hasAnyRole('ADMIN')") |     @PreAuthorize("hasAnyRole('ADMIN')") | ||||||
|     @PutMapping("/tunnel/{tunnelId}/add") |     @PutMapping("/tunnel/{tunnelId}/add") | ||||||
|     public ResponseEntity<Map<String, Object>> addTunnelconfiguration(@PathVariable String tunnelId, @RequestBody Ingress ingress) throws JsonProcessingException { |     public ResponseEntity<Map<String, Object>> addTunnelconfiguration(@PathVariable String tunnelId, @RequestBody Ingress ingress) throws JsonProcessingException { | ||||||
| 
 | 
 | ||||||
|         String url = "https://api.cloudflare.com/client/v4/accounts/" + cloudflareConfig.getAccountId() + "/cfd_tunnel/" + tunnelId + "/configurations"; |         ResponseEntity<TunnelResponse> responseEntity = cloudflareAPIService.getCloudflareTunnelConfigurations(tunnelId, restTemplateConfig.restTemplate(), TunnelResponse.class); | ||||||
| 
 |  | ||||||
|         // * * Getting existing public hostname mappings |  | ||||||
|         HttpHeaders httpHeaders = authKeyEmailHeader.getHttpHeaders(); |  | ||||||
|         HttpEntity<String> httpEntity = new HttpEntity<>("",httpHeaders); |  | ||||||
|         ResponseEntity<TunnelResponse> responseEntity = restTemplateConfig.restTemplate().exchange(url, HttpMethod.GET, httpEntity, TunnelResponse.class); |  | ||||||
| 
 | 
 | ||||||
|         // * * Inserting new ingress value at second-to last position in list |         // * * Inserting new ingress value at second-to last position in list | ||||||
|         Config config = responseEntity.getBody().getResult().getConfig(); |         Config config = responseEntity.getBody().getResult().getConfig(); | ||||||
| @ -105,9 +94,7 @@ public class TunnelController implements ErrorController { | |||||||
|         response_ingress.add(response_ingress.size()-1, ingress); |         response_ingress.add(response_ingress.size()-1, ingress); | ||||||
| 
 | 
 | ||||||
|         // * * Hitting put endpoint |         // * * Hitting put endpoint | ||||||
|         httpHeaders.setContentType(MediaType.APPLICATION_JSON); |         ResponseEntity<TunnelResponse> response = cloudflareAPIService.putCloudflareTunnelConfigurations(tunnelId, restTemplateConfig.restTemplate(), TunnelResponse.class, config); | ||||||
|         HttpEntity<Config> entity = new HttpEntity<>(config, httpHeaders); |  | ||||||
|         ResponseEntity<Map> response = restTemplateConfig.restTemplate().exchange(url, HttpMethod.PUT, entity, Map.class); |  | ||||||
| 
 | 
 | ||||||
|         // * * Displaying response |         // * * Displaying response | ||||||
|         Map<String, Object> jsonResponse = new HashMap<>(); |         Map<String, Object> jsonResponse = new HashMap<>(); | ||||||
| @ -121,12 +108,7 @@ public class TunnelController implements ErrorController { | |||||||
|     @PutMapping("/tunnel/{tunnelId}/delete") |     @PutMapping("/tunnel/{tunnelId}/delete") | ||||||
|     public ResponseEntity<Map<String, Object>> deleteTunnelConfiguration(@PathVariable String tunnelId, @RequestBody Ingress ingress) throws JsonProcessingException { |     public ResponseEntity<Map<String, Object>> deleteTunnelConfiguration(@PathVariable String tunnelId, @RequestBody Ingress ingress) throws JsonProcessingException { | ||||||
| 
 | 
 | ||||||
|         String url = "https://api.cloudflare.com/client/v4/accounts/" + cloudflareConfig.getAccountId() + "/cfd_tunnel/" + tunnelId + "/configurations"; |         ResponseEntity<TunnelResponse> responseEntity = cloudflareAPIService.getCloudflareTunnelConfigurations(tunnelId, restTemplateConfig.restTemplate(), TunnelResponse.class); | ||||||
| 
 |  | ||||||
|         // * * Getting existing public hostname mappings |  | ||||||
|         HttpHeaders httpHeaders = authKeyEmailHeader.getHttpHeaders(); |  | ||||||
|         HttpEntity<String> httpEntity = new HttpEntity<>("",httpHeaders); |  | ||||||
|         ResponseEntity<TunnelResponse> responseEntity = restTemplateConfig.restTemplate().exchange(url, HttpMethod.GET, httpEntity, TunnelResponse.class); |  | ||||||
| 
 | 
 | ||||||
|         // * * Deleting the selected ingress value |         // * * Deleting the selected ingress value | ||||||
|         Config config = responseEntity.getBody().getResult().getConfig(); |         Config config = responseEntity.getBody().getResult().getConfig(); | ||||||
| @ -134,9 +116,7 @@ public class TunnelController implements ErrorController { | |||||||
|         Boolean result = Ingress.deleteByHostName(response_ingress, ingress.getHostname()); |         Boolean result = Ingress.deleteByHostName(response_ingress, ingress.getHostname()); | ||||||
| 
 | 
 | ||||||
|         // * * Hitting put endpoint |         // * * Hitting put endpoint | ||||||
|         httpHeaders.setContentType(MediaType.APPLICATION_JSON); |         ResponseEntity<TunnelResponse> response = cloudflareAPIService.putCloudflareTunnelConfigurations(tunnelId, restTemplateConfig.restTemplate(), TunnelResponse.class, config); | ||||||
|         HttpEntity<Config> entity = new HttpEntity<>(config, httpHeaders); |  | ||||||
|         ResponseEntity<Map> response = restTemplateConfig.restTemplate().exchange(url, HttpMethod.PUT, entity, Map.class); |  | ||||||
| 
 | 
 | ||||||
|         // * * Displaying response |         // * * Displaying response | ||||||
|         Map<String, Object> jsonResponse = new HashMap<>(); |         Map<String, Object> jsonResponse = new HashMap<>(); | ||||||
|  | |||||||
| @ -1,8 +1,17 @@ | |||||||
| package com.hithomelabs.CFTunnels.Models; | package com.hithomelabs.CFTunnels.Models; | ||||||
| 
 | 
 | ||||||
|  | import lombok.AllArgsConstructor; | ||||||
|  | import lombok.Getter; | ||||||
|  | import lombok.NoArgsConstructor; | ||||||
|  | import lombok.Setter; | ||||||
|  | 
 | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.Map; | import java.util.Map; | ||||||
| 
 | 
 | ||||||
|  | @Getter | ||||||
|  | @Setter | ||||||
|  | @NoArgsConstructor | ||||||
|  | @AllArgsConstructor | ||||||
| public class TunnelResponse { | public class TunnelResponse { | ||||||
| 
 | 
 | ||||||
|     private List<Map<String, Object>> errors; |     private List<Map<String, Object>> errors; | ||||||
| @ -12,45 +21,4 @@ public class TunnelResponse { | |||||||
|     private Boolean success; |     private Boolean success; | ||||||
| 
 | 
 | ||||||
|     private Result result; |     private Result result; | ||||||
| 
 |  | ||||||
|     public Result getResult() { |  | ||||||
|         return result; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void setResult(Result result) { |  | ||||||
|         this.result = result; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public List<Map<String, Object>> getErrors() { |  | ||||||
|         return errors; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void setErrors(List<Map<String, Object>> errors) { |  | ||||||
|         this.errors = errors; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public List<Map<String, Object>> getMessages() { |  | ||||||
|         return messages; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void setMessages(List<Map<String, Object>> messages) { |  | ||||||
|         this.messages = messages; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public boolean isSuccess() { |  | ||||||
|         return success; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void setSuccess(boolean success) { |  | ||||||
|         this.success = success; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public Boolean getSuccess() { |  | ||||||
|         return success; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void setSuccess(Boolean success) { |  | ||||||
|         this.success = success; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -0,0 +1,61 @@ | |||||||
|  | package com.hithomelabs.CFTunnels.Services; | ||||||
|  | 
 | ||||||
|  | import com.hithomelabs.CFTunnels.Config.CloudflareConfig; | ||||||
|  | import com.hithomelabs.CFTunnels.Headers.AuthKeyEmailHeader; | ||||||
|  | import com.hithomelabs.CFTunnels.Models.Config; | ||||||
|  | import org.springframework.beans.factory.annotation.Autowired; | ||||||
|  | import org.springframework.http.*; | ||||||
|  | import org.springframework.stereotype.Service; | ||||||
|  | import org.springframework.web.client.RestTemplate; | ||||||
|  | 
 | ||||||
|  | import java.util.Map; | ||||||
|  | 
 | ||||||
|  | @Service | ||||||
|  | public class CloudflareAPIService { | ||||||
|  | 
 | ||||||
|  |     @Autowired | ||||||
|  |     CloudflareConfig cloudflareConfig; | ||||||
|  | 
 | ||||||
|  |     @Autowired | ||||||
|  |     AuthKeyEmailHeader authKeyEmailHeader; | ||||||
|  | 
 | ||||||
|  |     @Autowired | ||||||
|  |     RestTemplate restTemplate; | ||||||
|  | 
 | ||||||
|  |     public ResponseEntity<Map> getCloudflareTunnels() { | ||||||
|  | 
 | ||||||
|  |         // * * Resource URL to hit get request at | ||||||
|  |         String url = "https://api.cloudflare.com/client/v4/accounts/" + cloudflareConfig.getAccountId() + "/cfd_tunnel"; | ||||||
|  | 
 | ||||||
|  |         HttpEntity<String> httpEntity = new HttpEntity<>("", authKeyEmailHeader.getHttpHeaders()); | ||||||
|  |         ResponseEntity<Map> responseEntity = restTemplate.exchange(url, HttpMethod.GET, httpEntity, Map.class); | ||||||
|  |         return responseEntity; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public <T> ResponseEntity<T> getCloudflareTunnelConfigurations(String tunnelId, RestTemplate restTemplate, Class<T> responseType) { | ||||||
|  | 
 | ||||||
|  |         // * * Resource URL to hit get request at | ||||||
|  |         String url = "https://api.cloudflare.com/client/v4/accounts/" + cloudflareConfig.getAccountId() + "/cfd_tunnel/" + tunnelId + "/configurations"; | ||||||
|  | 
 | ||||||
|  |         HttpEntity<String> httpEntity = new HttpEntity<>("",authKeyEmailHeader.getHttpHeaders()); | ||||||
|  |         ResponseEntity<T> responseEntity = restTemplate.exchange(url, HttpMethod.GET, httpEntity, responseType); | ||||||
|  |         return responseEntity; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public <T> ResponseEntity<T> putCloudflareTunnelConfigurations(String tunnelId, RestTemplate restTemplate, Class<T> responseType, Config config) { | ||||||
|  | 
 | ||||||
|  |         // * * Resource URL to hit get request at | ||||||
|  |         String url = "https://api.cloudflare.com/client/v4/accounts/" + cloudflareConfig.getAccountId() + "/cfd_tunnel/" + tunnelId + "/configurations"; | ||||||
|  | 
 | ||||||
|  |         HttpHeaders httpHeaders = authKeyEmailHeader.getHttpHeaders(); | ||||||
|  |         httpHeaders.setContentType(MediaType.APPLICATION_JSON); | ||||||
|  |         HttpEntity<Config> entity = new HttpEntity<>(config, httpHeaders); | ||||||
|  |         ResponseEntity<T> responseEntity = restTemplate.exchange(url, HttpMethod.PUT, entity, responseType); | ||||||
|  |         return responseEntity; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -1 +1,10 @@ | |||||||
| api.corsResolveUrl=http://localhost:8080 | api.baseUrl=http://localhost:8080 | ||||||
|  | 
 | ||||||
|  | management.health.db.enabled=true | ||||||
|  | management.endpoints.web.exposure.include=health | ||||||
|  | management.endpoint.health.show-details=always | ||||||
|  | 
 | ||||||
|  | logging.level.org.hibernate.SQL=DEBUG | ||||||
|  | debug=true | ||||||
|  | 
 | ||||||
|  | spring.datasource.url=jdbc:postgresql://localhost:5432/cftunnel | ||||||
|  | |||||||
| @ -1 +1 @@ | |||||||
| api.corsResolveUrl=https://cftunnels.hithomelabs.com | api.baseUrl=https://cftunnels.hithomelabs.com | ||||||
| @ -1 +1 @@ | |||||||
| api.corsResolveUrl=https://testcf.hithomelabs.com | api.baseUrl=https://testcf.hithomelabs.com | ||||||
| @ -4,6 +4,12 @@ cloudflare.apiKey=${CLOUDFLARE_API_KEY} | |||||||
| cloudflare.email=${CLOUDFLARE_EMAIL} | cloudflare.email=${CLOUDFLARE_EMAIL} | ||||||
| spring.profiles.active=${ENV} | spring.profiles.active=${ENV} | ||||||
| 
 | 
 | ||||||
|  | # set root level | ||||||
|  | logging.level.root=INFO | ||||||
|  | # package-specific | ||||||
|  | logging.level.org.springframework=TRACE | ||||||
|  | logging.level.com.myapp=INFO | ||||||
|  | 
 | ||||||
| / * * Masking sure app works behind a reverse proxy | / * * Masking sure app works behind a reverse proxy | ||||||
| server.forward-headers-strategy=framework | server.forward-headers-strategy=framework | ||||||
| 
 | 
 | ||||||
| @ -16,4 +22,16 @@ spring.security.oauth2.client.provider.cftunnels.authorization-uri=https://auth. | |||||||
| spring.security.oauth2.client.provider.cftunnels.token-uri=https://auth.hithomelabs.com/application/o/token/ | spring.security.oauth2.client.provider.cftunnels.token-uri=https://auth.hithomelabs.com/application/o/token/ | ||||||
| spring.security.oauth2.client.provider.cftunnels.user-info-uri=https://auth.hithomelabs.com/application/o/userinfo/ | spring.security.oauth2.client.provider.cftunnels.user-info-uri=https://auth.hithomelabs.com/application/o/userinfo/ | ||||||
| spring.security.oauth2.client.provider.cftunnels.jwk-set-uri=https://auth.hithomelabs.com/application/o/cftunnels/jwks/ | spring.security.oauth2.client.provider.cftunnels.jwk-set-uri=https://auth.hithomelabs.com/application/o/cftunnels/jwks/ | ||||||
| spring.security.oauth2.client.provider.cftunnels.issuer-uri=https://auth.hithomelabs.com/application/o/cftunnels/ | spring.security.oauth2.client.provider.cftunnels.issuer-uri=https://auth.hithomelabs.com/application/o/cftunnels/ | ||||||
|  | 
 | ||||||
|  | spring.datasource.url=jdbc:postgresql://192.168.0.100:5432/cftunnel | ||||||
|  | spring.datasource.username=${POSTGRES_USERNAME} | ||||||
|  | spring.datasource.password=${POSTGRES_PASSWORD} | ||||||
|  | spring.datasource.driver-class-name=org.postgresql.Driver | ||||||
|  | spring.sql.init.mode=never | ||||||
|  | 
 | ||||||
|  | spring.jpa.hibernate.ddl-auto=update | ||||||
|  | spring.jpa.show-sql=true | ||||||
|  | spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect | ||||||
|  | 
 | ||||||
|  | spring.jpa.open-in-view=false | ||||||
							
								
								
									
										37
									
								
								src/main/resources/schema.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/main/resources/schema.sql
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,37 @@ | |||||||
|  | -- schema.sql | ||||||
|  | 
 | ||||||
|  | -- Roles table | ||||||
|  | CREATE TABLE IF NOT EXISTS roles ( | ||||||
|  |     role_id SERIAL PRIMARY KEY, | ||||||
|  |     role_name VARCHAR(50) UNIQUE NOT NULL | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | -- Users table | ||||||
|  | CREATE TABLE IF NOT EXISTS users ( | ||||||
|  |     user_id SERIAL PRIMARY KEY, | ||||||
|  |     user_name VARCHAR(100) NOT NULL, | ||||||
|  |     password VARCHAR(255) NOT NULL | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | -- User-Role Mapping table (many-to-many relationship) | ||||||
|  | CREATE TABLE IF NOT EXISTS user_role_mapping ( | ||||||
|  |     mapping_id SERIAL PRIMARY KEY, | ||||||
|  |     user_id INTEGER NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, | ||||||
|  |     role_id INTEGER NOT NULL REFERENCES roles(role_id) ON DELETE CASCADE | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | -- Tunnels table | ||||||
|  | CREATE TABLE IF NOT EXISTS tunnels ( | ||||||
|  |     tunnel_id SERIAL PRIMARY KEY, | ||||||
|  |     tunnel_name VARCHAR(100) NOT NULL, | ||||||
|  |     tunnel_type VARCHAR(50) NOT NULL | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | -- Mapping Requests table | ||||||
|  | CREATE TABLE IF NOT EXISTS mapping_requests ( | ||||||
|  |     request_id SERIAL PRIMARY KEY, | ||||||
|  |     request_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||||||
|  |     status VARCHAR(20) NOT NULL, | ||||||
|  |     user_id INTEGER REFERENCES users(user_id) ON DELETE SET NULL, | ||||||
|  |     tunnel_id INTEGER REFERENCES tunnels(tunnel_id) ON DELETE SET NULL | ||||||
|  | ); | ||||||
| @ -0,0 +1,219 @@ | |||||||
|  | package com.hithomelabs.CFTunnels.Controllers; | ||||||
|  | 
 | ||||||
|  | import com.fasterxml.jackson.databind.ObjectMapper; | ||||||
|  | import com.hithomelabs.CFTunnels.Config.AuthoritiesToGroupMapping; | ||||||
|  | import com.hithomelabs.CFTunnels.Config.CloudflareConfig; | ||||||
|  | import com.hithomelabs.CFTunnels.Config.RestTemplateConfig; | ||||||
|  | import com.hithomelabs.CFTunnels.Headers.AuthKeyEmailHeader; | ||||||
|  | import com.hithomelabs.CFTunnels.Models.Authorities; | ||||||
|  | import com.hithomelabs.CFTunnels.Models.Config; | ||||||
|  | import com.hithomelabs.CFTunnels.Models.Groups; | ||||||
|  | import com.hithomelabs.CFTunnels.Models.TunnelResponse; | ||||||
|  | import com.hithomelabs.CFTunnels.Services.CloudflareAPIService; | ||||||
|  | import org.junit.jupiter.api.DisplayName; | ||||||
|  | import org.junit.jupiter.api.Test; | ||||||
|  | import org.springframework.beans.factory.annotation.Autowired; | ||||||
|  | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; | ||||||
|  | import org.springframework.http.*; | ||||||
|  | import org.springframework.security.core.GrantedAuthority; | ||||||
|  | import org.springframework.security.core.authority.SimpleGrantedAuthority; | ||||||
|  | import org.springframework.security.oauth2.core.oidc.OidcIdToken; | ||||||
|  | import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; | ||||||
|  | import org.springframework.test.context.bean.override.mockito.MockitoBean; | ||||||
|  | import org.springframework.test.web.servlet.MockMvc; | ||||||
|  | import org.springframework.test.web.servlet.result.MockMvcResultMatchers; | ||||||
|  | import org.springframework.web.client.RestTemplate; | ||||||
|  | 
 | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.time.Instant; | ||||||
|  | import java.util.*; | ||||||
|  | 
 | ||||||
|  | import static com.hithomelabs.CFTunnels.TestUtils.Util.getClassPathDataResource; | ||||||
|  | import static org.hamcrest.core.IsIterableContaining.hasItem; | ||||||
|  | import static org.mockito.ArgumentMatchers.any; | ||||||
|  | import static org.mockito.ArgumentMatchers.eq; | ||||||
|  | import static org.mockito.Mockito.when; | ||||||
|  | import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; | ||||||
|  | import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.oauth2Login; | ||||||
|  | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; | ||||||
|  | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; | ||||||
|  | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; | ||||||
|  | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; | ||||||
|  | import static org.hamcrest.Matchers.not; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @WebMvcTest(TunnelController.class) | ||||||
|  | class TunnelControllerTest { | ||||||
|  | 
 | ||||||
|  |     @Autowired | ||||||
|  |     MockMvc mockMvc; | ||||||
|  | 
 | ||||||
|  |     @MockitoBean | ||||||
|  |     AuthoritiesToGroupMapping authoritiesToGroupMapping; | ||||||
|  | 
 | ||||||
|  |     @MockitoBean | ||||||
|  |     CloudflareConfig cloudflareConfig; | ||||||
|  | 
 | ||||||
|  |     @MockitoBean | ||||||
|  |     AuthKeyEmailHeader authKeyEmailHeader; | ||||||
|  | 
 | ||||||
|  |     @MockitoBean | ||||||
|  |     RestTemplate restTemplate; | ||||||
|  | 
 | ||||||
|  |     @MockitoBean | ||||||
|  |     CloudflareAPIService cloudflareAPIService; | ||||||
|  | 
 | ||||||
|  |     @MockitoBean | ||||||
|  |     RestTemplateConfig restTemplateConfig; | ||||||
|  | 
 | ||||||
|  |     private static final String tunnelResponseSmallIngressFile = "tunnelResponseSmallIngress.json"; | ||||||
|  | 
 | ||||||
|  |     private static final String tunnelResponseLargeIngressFile = "tunnelResponseLargeIngress.json"; | ||||||
|  | 
 | ||||||
|  |     private static final String withAdditionalIngress; | ||||||
|  | 
 | ||||||
|  |     static { | ||||||
|  |         try { | ||||||
|  |             withAdditionalIngress = getClassPathDataResource(tunnelResponseLargeIngressFile); | ||||||
|  |         } catch (IOException e) { | ||||||
|  |             throw new RuntimeException(e); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static final String withoutAdditionalIngress; | ||||||
|  |     static { | ||||||
|  |         try { | ||||||
|  |             withoutAdditionalIngress = getClassPathDataResource(tunnelResponseSmallIngressFile); | ||||||
|  |         } catch (IOException e) { | ||||||
|  |             throw new RuntimeException(e); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static final String ingressJson = """ | ||||||
|  |             { | ||||||
|  |                 "service": "http://192.168.0.100:3457", | ||||||
|  |                 "hostname": "random.hithomelabs.com", | ||||||
|  |                 "originRequest": {} | ||||||
|  |             } | ||||||
|  |             """; | ||||||
|  | 
 | ||||||
|  |     private DefaultOidcUser buildOidcUser(String username, String role) { | ||||||
|  | 
 | ||||||
|  |         when(authoritiesToGroupMapping.getAuthorityForGroup()).thenReturn(Map.of(Groups.GITEA_USER, new HashSet<>(Set.of(new SimpleGrantedAuthority(Authorities.ROLE_USER))), | ||||||
|  |                 Groups.POWER_USER, new HashSet<>(Set.of(new SimpleGrantedAuthority(Authorities.ROLE_USER))), | ||||||
|  |                 Groups.HOMELAB_DEVELOPER, new HashSet<>(Set.of(new SimpleGrantedAuthority(Authorities.ROLE_DEVELOPER))), | ||||||
|  |                 Groups.SYSTEM_ADMIN, new HashSet<>(Set.of(new SimpleGrantedAuthority(Authorities.ROLE_APPROVER), new SimpleGrantedAuthority(Authorities.ROLE_ADMIN))))); | ||||||
|  | 
 | ||||||
|  |         Map<String, Set<GrantedAuthority>> roleAuthorityMapping = authoritiesToGroupMapping.getAuthorityForGroup(); | ||||||
|  |         List<GrantedAuthority> authorities = roleAuthorityMapping.get(role).stream().toList(); | ||||||
|  | 
 | ||||||
|  |         OidcIdToken idToken = new OidcIdToken( | ||||||
|  |                 "mock-token", | ||||||
|  |                 Instant.now(), | ||||||
|  |                 Instant.now().plusSeconds(3600), | ||||||
|  |                 Map.of("preferred_username", username, "sub", username) | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         return new DefaultOidcUser(authorities, idToken, "preferred_username"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     @DisplayName("should return appropriate user roles when use belongs to group GITEA_USER") | ||||||
|  |     public void testWhoAmI_user() throws Exception { | ||||||
|  |         mockMvc.perform(get("/cloudflare/whoami") | ||||||
|  |                         .with(oauth2Login().oauth2User(buildOidcUser("username", Groups.GITEA_USER)))) | ||||||
|  |                 .andExpect(status().isOk()) | ||||||
|  |                 .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) | ||||||
|  |                 .andExpect(jsonPath("$.username").value("username")) | ||||||
|  |                 .andExpect(jsonPath("$.roles", hasItem("ROLE_USER"))); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     @DisplayName("should hit tunnels endpoint successfully with ROLE_USER") | ||||||
|  |     public void testGetTunnelsForRoleUser() throws Exception { | ||||||
|  | 
 | ||||||
|  |         when(cloudflareConfig.getAccountId()).thenReturn("abc123"); | ||||||
|  |         HttpHeaders headers = new HttpHeaders(); | ||||||
|  |         headers.set("X-Auth-Email", "me@example.com"); | ||||||
|  |         when(authKeyEmailHeader.getHttpHeaders()).thenReturn(headers); | ||||||
|  | 
 | ||||||
|  |         Map<String, Object> tunnelData = Map.of("tunnels", List.of(Map.of("id", "50df9101-f625-4618-b7c5-100338a57124"))); | ||||||
|  |         ResponseEntity<Map> mockResponse = new ResponseEntity<>(tunnelData, HttpStatus.OK); | ||||||
|  | 
 | ||||||
|  |         when(cloudflareAPIService.getCloudflareTunnels()).thenReturn(mockResponse); | ||||||
|  | 
 | ||||||
|  |         mockMvc.perform(get("/cloudflare/tunnels") | ||||||
|  |                         .with(oauth2Login().oauth2User(buildOidcUser("username", Groups.GITEA_USER)))) | ||||||
|  |                 .andExpect(status().isOk()) | ||||||
|  |                 .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) | ||||||
|  |                 .andExpect(jsonPath("$.data.tunnels[0].id").value("50df9101-f625-4618-b7c5-100338a57124")); | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void getTunnelConfigurations() throws Exception { | ||||||
|  | 
 | ||||||
|  |         Map<String, Object> tunnelData = Map.of("config", Map.of("result", "success", "ingress", "sample ingress object")); | ||||||
|  |         ResponseEntity<Map> mockResponse = new ResponseEntity<>(tunnelData, HttpStatus.OK); | ||||||
|  | 
 | ||||||
|  |         when(cloudflareAPIService.getCloudflareTunnelConfigurations(eq("sampleTunnelId"), any(RestTemplate.class), eq(Map.class))).thenReturn(mockResponse); | ||||||
|  | 
 | ||||||
|  |         mockMvc.perform(get("/cloudflare/tunnel/{tunnelId}", "sampleTunnelId") | ||||||
|  |                         .with(oauth2Login().oauth2User(buildOidcUser("username", Groups.HOMELAB_DEVELOPER)))) | ||||||
|  |                 .andExpect(status().isOk()) | ||||||
|  |                 .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) | ||||||
|  |                 .andExpect(jsonPath("$.data.config.ingress").value("sample ingress object")); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void addTunnelconfiguration() throws Exception { | ||||||
|  | 
 | ||||||
|  |         when(restTemplateConfig.restTemplate()).thenReturn(new RestTemplate()); | ||||||
|  | 
 | ||||||
|  |         ObjectMapper mapper = new ObjectMapper(); | ||||||
|  |         TunnelResponse tunnelStateBefore = mapper.readValue(withoutAdditionalIngress, TunnelResponse.class); | ||||||
|  |         ResponseEntity<TunnelResponse> tunnelResponseBefore = new ResponseEntity<>(tunnelStateBefore, HttpStatus.OK); | ||||||
|  | 
 | ||||||
|  |         when(cloudflareAPIService.getCloudflareTunnelConfigurations(eq("sampleTunnelId"), any(RestTemplate.class), eq(TunnelResponse.class))).thenReturn(tunnelResponseBefore); | ||||||
|  | 
 | ||||||
|  |         TunnelResponse expectedTunnelConfig = mapper.readValue(withAdditionalIngress, TunnelResponse.class); | ||||||
|  |         ResponseEntity<TunnelResponse> expectedHttpTunnelResponse = new ResponseEntity<>(expectedTunnelConfig, HttpStatus.OK); | ||||||
|  |         when(cloudflareAPIService.putCloudflareTunnelConfigurations(eq("sampleTunnelId"), any(RestTemplate.class), eq(TunnelResponse.class), any(Config.class))).thenReturn(expectedHttpTunnelResponse); | ||||||
|  | 
 | ||||||
|  |         mockMvc.perform(put("/cloudflare/tunnel/{tunnelId}/add", "sampleTunnelId") | ||||||
|  |                         .with(oauth2Login().oauth2User(buildOidcUser("admin", Groups.SYSTEM_ADMIN))) | ||||||
|  |                         .with(csrf()) | ||||||
|  |                         .contentType(MediaType.APPLICATION_JSON) | ||||||
|  |                         .content(ingressJson)) | ||||||
|  |                 .andExpect(status().isOk()) | ||||||
|  |                 .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) | ||||||
|  |                 .andExpect(jsonPath("$.data.result.config.ingress[*].hostname", hasItem("random.hithomelabs.com"))); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void deleteTunnelConfiguration() throws Exception { | ||||||
|  | 
 | ||||||
|  |         when(restTemplateConfig.restTemplate()).thenReturn(new RestTemplate()); | ||||||
|  | 
 | ||||||
|  |         ObjectMapper mapper = new ObjectMapper(); | ||||||
|  |         TunnelResponse tunnelStateBefore = mapper.readValue(withAdditionalIngress, TunnelResponse.class); | ||||||
|  |         ResponseEntity<TunnelResponse> tunnelResponseBefore = new ResponseEntity<>(tunnelStateBefore, HttpStatus.OK); | ||||||
|  | 
 | ||||||
|  |         when(cloudflareAPIService.getCloudflareTunnelConfigurations(eq("sampleTunnelId"), any(RestTemplate.class), eq(TunnelResponse.class))).thenReturn(tunnelResponseBefore); | ||||||
|  | 
 | ||||||
|  |         TunnelResponse expectedTunnelConfig = mapper.readValue(withoutAdditionalIngress, TunnelResponse.class); | ||||||
|  |         ResponseEntity<TunnelResponse> expectedHttpTunnelResponse = new ResponseEntity<>(expectedTunnelConfig, HttpStatus.OK); | ||||||
|  |         when(cloudflareAPIService.putCloudflareTunnelConfigurations(eq("sampleTunnelId"), any(RestTemplate.class), eq(TunnelResponse.class), any(Config.class))).thenReturn(expectedHttpTunnelResponse); | ||||||
|  | 
 | ||||||
|  |         mockMvc.perform(put("/cloudflare/tunnel/{tunnelId}/delete", "sampleTunnelId") | ||||||
|  |                         .with(oauth2Login().oauth2User(buildOidcUser("admin", Groups.SYSTEM_ADMIN))) | ||||||
|  |                         .with(csrf()) | ||||||
|  |                         .contentType(MediaType.APPLICATION_JSON) | ||||||
|  |                         .content(ingressJson)) | ||||||
|  |                 .andExpect(status().isOk()) | ||||||
|  |                 .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) | ||||||
|  |                 .andExpect(jsonPath("$.data.result.config.ingress[*].hostname", not(hasItem("random.hithomelabs.com")))); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -0,0 +1,117 @@ | |||||||
|  | package com.hithomelabs.CFTunnels.Services; | ||||||
|  | 
 | ||||||
|  | import com.fasterxml.jackson.core.JsonProcessingException; | ||||||
|  | import com.fasterxml.jackson.databind.ObjectMapper; | ||||||
|  | import com.hithomelabs.CFTunnels.Config.CloudflareConfig; | ||||||
|  | import com.hithomelabs.CFTunnels.Headers.AuthKeyEmailHeader; | ||||||
|  | import com.hithomelabs.CFTunnels.Models.Config; | ||||||
|  | import com.hithomelabs.CFTunnels.Models.TunnelResponse; | ||||||
|  | import org.junit.jupiter.api.Test; | ||||||
|  | import org.junit.jupiter.api.extension.ExtendWith; | ||||||
|  | import org.mockito.InjectMocks; | ||||||
|  | import org.mockito.Mock; | ||||||
|  | import org.mockito.junit.jupiter.MockitoExtension; | ||||||
|  | import org.springframework.http.*; | ||||||
|  | import org.springframework.web.client.RestTemplate; | ||||||
|  | 
 | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.Map; | ||||||
|  | 
 | ||||||
|  | import static com.hithomelabs.CFTunnels.TestUtils.Util.getClassPathDataResource; | ||||||
|  | import static org.junit.jupiter.api.Assertions.assertEquals; | ||||||
|  | import static org.mockito.ArgumentMatchers.any; | ||||||
|  | import static org.mockito.ArgumentMatchers.eq; | ||||||
|  | import static org.mockito.Mockito.when; | ||||||
|  | 
 | ||||||
|  | @ExtendWith(MockitoExtension.class) | ||||||
|  | class CloudflareAPIServiceTest { | ||||||
|  | 
 | ||||||
|  |     @InjectMocks | ||||||
|  |     private CloudflareAPIService cloudflareAPIService; | ||||||
|  | 
 | ||||||
|  |     @Mock | ||||||
|  |     AuthKeyEmailHeader authKeyEmailHeader; | ||||||
|  | 
 | ||||||
|  |     @Mock | ||||||
|  |     private RestTemplate restTemplate; | ||||||
|  | 
 | ||||||
|  |     @Mock | ||||||
|  |     CloudflareConfig cloudflareConfig; | ||||||
|  | 
 | ||||||
|  |     private static final String tunnelResponseLargeIngressFile = "tunnelResponseLargeIngress.json"; | ||||||
|  | 
 | ||||||
|  |     private static final String bigTunnelResponse; | ||||||
|  | 
 | ||||||
|  |     static { | ||||||
|  |         try { | ||||||
|  |             bigTunnelResponse = getClassPathDataResource(tunnelResponseLargeIngressFile); | ||||||
|  |         } catch (IOException e) { | ||||||
|  |             throw new RuntimeException(e); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void testGetCloudflareTunnels() { | ||||||
|  | 
 | ||||||
|  |         when(cloudflareConfig.getAccountId()).thenReturn("account-123"); | ||||||
|  |         when(authKeyEmailHeader.getHttpHeaders()).thenReturn(new HttpHeaders()); | ||||||
|  |         Map<String, Object> mockBody = Map.of("tunnels", List.of(Map.of("id", "t1"))); | ||||||
|  |         ResponseEntity<Map> mockResponse = new ResponseEntity<>(mockBody, HttpStatus.OK); | ||||||
|  | 
 | ||||||
|  |         when(restTemplate.exchange( | ||||||
|  |                 any(String.class), | ||||||
|  |                 eq(HttpMethod.GET), | ||||||
|  |                 any(HttpEntity.class), | ||||||
|  |                 eq(Map.class) | ||||||
|  |         )).thenReturn(mockResponse); | ||||||
|  | 
 | ||||||
|  |         ResponseEntity<Map> response = cloudflareAPIService.getCloudflareTunnels(); | ||||||
|  |         assertEquals(HttpStatus.OK, response.getStatusCode()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void getCloudflareTunnelConfigurations() throws JsonProcessingException { | ||||||
|  | 
 | ||||||
|  |         when(cloudflareConfig.getAccountId()).thenReturn("account-123"); | ||||||
|  |         when(authKeyEmailHeader.getHttpHeaders()).thenReturn(new HttpHeaders()); | ||||||
|  | 
 | ||||||
|  |         TunnelResponse tunnelResponse = new ObjectMapper().readValue(bigTunnelResponse, TunnelResponse.class); | ||||||
|  |         ResponseEntity<TunnelResponse> tunnelResponseResponseEntity = new ResponseEntity<>(tunnelResponse, HttpStatus.OK); | ||||||
|  | 
 | ||||||
|  |         when(restTemplate.exchange( | ||||||
|  |                 any(String.class), | ||||||
|  |                 eq(HttpMethod.GET), | ||||||
|  |                 any(HttpEntity.class), | ||||||
|  |                 eq(TunnelResponse.class) | ||||||
|  |         )).thenReturn(tunnelResponseResponseEntity); | ||||||
|  | 
 | ||||||
|  |         ResponseEntity<TunnelResponse> response = cloudflareAPIService.getCloudflareTunnelConfigurations("sampleTunnelID", restTemplate, TunnelResponse.class); | ||||||
|  |         assertEquals(HttpStatus.OK, response.getStatusCode()); | ||||||
|  |         assertEquals(response.getBody().getResult().getConfig().getIngress().get(0).getHostname(), "giteabkp.hithomelabs.com"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     void putCloudflareTunnelConfigurations() throws JsonProcessingException { | ||||||
|  | 
 | ||||||
|  |         when(cloudflareConfig.getAccountId()).thenReturn("account-123"); | ||||||
|  |         when(authKeyEmailHeader.getHttpHeaders()).thenReturn(new HttpHeaders()); | ||||||
|  | 
 | ||||||
|  |         TunnelResponse tunnelResponse = new ObjectMapper().readValue(bigTunnelResponse, TunnelResponse.class); | ||||||
|  |         ResponseEntity<TunnelResponse> tunnelResponseResponseEntity = new ResponseEntity<>(tunnelResponse, HttpStatus.OK); | ||||||
|  | 
 | ||||||
|  |         Config config = tunnelResponse.getResult().getConfig(); | ||||||
|  | 
 | ||||||
|  |         when(restTemplate.exchange( | ||||||
|  |                 any(String.class), | ||||||
|  |                 eq(HttpMethod.PUT), | ||||||
|  |                 any(HttpEntity.class), | ||||||
|  |                 eq(TunnelResponse.class) | ||||||
|  |         )).thenReturn(tunnelResponseResponseEntity); | ||||||
|  | 
 | ||||||
|  |         ResponseEntity<TunnelResponse> response = cloudflareAPIService.putCloudflareTunnelConfigurations("sampleTunnelID", restTemplate, TunnelResponse.class, config); | ||||||
|  |         assertEquals(HttpStatus.OK, response.getStatusCode()); | ||||||
|  |         assertEquals(response.getBody().getResult().getConfig().getIngress().get(2).getHostname(), "random.hithomelabs.com"); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										17
									
								
								src/test/java/com/hithomelabs/CFTunnels/TestUtils/Util.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/test/java/com/hithomelabs/CFTunnels/TestUtils/Util.java
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | |||||||
|  | package com.hithomelabs.CFTunnels.TestUtils; | ||||||
|  | 
 | ||||||
|  | import org.springframework.core.io.ClassPathResource; | ||||||
|  | 
 | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.nio.charset.StandardCharsets; | ||||||
|  | import java.nio.file.Files; | ||||||
|  | 
 | ||||||
|  | public class Util { | ||||||
|  | 
 | ||||||
|  |     public static String getClassPathDataResource(String filename) throws IOException { | ||||||
|  |         return Files.readString( | ||||||
|  |                 new ClassPathResource(String.format("data/%s", filename)).getFile().toPath(), | ||||||
|  |                 StandardCharsets.UTF_8); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										38
									
								
								src/test/resources/data/tunnelResponseLargeIngress.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/test/resources/data/tunnelResponseLargeIngress.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,38 @@ | |||||||
|  | { | ||||||
|  |   "success": true, | ||||||
|  |   "errors": [], | ||||||
|  |   "messages": [], | ||||||
|  |   "result": { | ||||||
|  |     "tunnel_id": "50df9101-f625-4618-b7c5-100338a57124", | ||||||
|  |     "version": 63, | ||||||
|  |     "config": { | ||||||
|  |       "ingress": [ | ||||||
|  |         { | ||||||
|  |           "service": "http://192.168.0.100:8928", | ||||||
|  |           "hostname": "giteabkp.hithomelabs.com", | ||||||
|  |           "originRequest": {} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           "service": "https://192.168.0.100:9442", | ||||||
|  |           "hostname": "devdocker.hithomelabs.com", | ||||||
|  |           "originRequest": { | ||||||
|  |             "noTLSVerify": true | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           "service": "http://192.168.0.100:3457", | ||||||
|  |           "hostname": "random.hithomelabs.com", | ||||||
|  |           "originRequest": {} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           "service": "http_status:404" | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       "warp-routing": { | ||||||
|  |         "enabled": false | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "source": "cloudflare", | ||||||
|  |     "created_at": "2025-10-24T18:17:26.914217Z" | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										33
									
								
								src/test/resources/data/tunnelResponseSmallIngress.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/test/resources/data/tunnelResponseSmallIngress.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,33 @@ | |||||||
|  | { | ||||||
|  |   "success": true, | ||||||
|  |   "errors": [], | ||||||
|  |   "messages": [], | ||||||
|  |   "result": { | ||||||
|  |     "tunnel_id": "50df9101-f625-4618-b7c5-100338a57124", | ||||||
|  |     "version": 63, | ||||||
|  |     "config": { | ||||||
|  |       "ingress": [ | ||||||
|  |         { | ||||||
|  |           "service": "http://192.168.0.100:8928", | ||||||
|  |           "hostname": "giteabkp.hithomelabs.com", | ||||||
|  |           "originRequest": {} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           "service": "https://192.168.0.100:9442", | ||||||
|  |           "hostname": "devdocker.hithomelabs.com", | ||||||
|  |           "originRequest": { | ||||||
|  |             "noTLSVerify": true | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           "service": "http_status:404" | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       "warp-routing": { | ||||||
|  |         "enabled": false | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "source": "cloudflare", | ||||||
|  |     "created_at": "2025-10-24T18:17:26.914217Z" | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										13
									
								
								src/test/resources/docker-compose.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/test/resources/docker-compose.yaml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | |||||||
|  | services: | ||||||
|  |   postgres: | ||||||
|  |     image: postgres:15-alpine | ||||||
|  |     container_name: cftunnel-db-${ENV} | ||||||
|  |     environment: | ||||||
|  |       POSTGRES_DB: cftunnel | ||||||
|  |       POSTGRES_USER: ${POSTGRES_USERNAME} | ||||||
|  |       POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} | ||||||
|  |     restart: unless-stopped | ||||||
|  |     ports: | ||||||
|  |       - "${DB_PORT}:5432" | ||||||
|  |     volumes: | ||||||
|  |       - ${DB_PATH}:/var/lib/postgresql/data | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user