Showing preview only (1,447K chars total). Download the full file or copy to clipboard to get everything.
Repository: amak-tech/port-buddy
Branch: master
Commit: 01335f82f321
Files: 364
Total size: 1.3 MB
Directory structure:
gitextract_lb0oskn5/
├── .github/
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── .mvn/
│ └── wrapper/
│ └── maven-wrapper.properties
├── Dockerfile-cli
├── Dockerfile-eureka
├── Dockerfile-gateway
├── Dockerfile-net-proxy
├── Dockerfile-server
├── Dockerfile-ssl-service
├── Dockerfile-web
├── LICENSE
├── README.md
├── cli/
│ ├── pom.xml
│ └── src/
│ └── main/
│ ├── java/
│ │ └── tech/
│ │ └── amak/
│ │ └── portbuddy/
│ │ └── cli/
│ │ ├── PortBuddy.java
│ │ ├── config/
│ │ │ └── ConfigurationService.java
│ │ ├── tunnel/
│ │ │ ├── HttpTunnelClient.java
│ │ │ └── NetTunnelClient.java
│ │ ├── ui/
│ │ │ ├── ConsoleUi.java
│ │ │ ├── HttpLogSink.java
│ │ │ └── NetTrafficSink.java
│ │ └── utils/
│ │ ├── HttpUtils.java
│ │ └── JsonUtils.java
│ └── resources/
│ ├── META-INF/
│ │ └── native-image/
│ │ └── tech.amak/
│ │ └── port-buddy-cli/
│ │ ├── reflect-config.json
│ │ └── resource-config.json
│ ├── application-dev.yml
│ ├── application.yml
│ └── logback.xml
├── common/
│ ├── pom.xml
│ └── src/
│ └── main/
│ └── java/
│ └── tech/
│ └── amak/
│ └── portbuddy/
│ └── common/
│ ├── ClientConfig.java
│ ├── Plan.java
│ ├── TunnelType.java
│ ├── dto/
│ │ ├── DnsInstructionsEmailRequest.java
│ │ ├── ExposeRequest.java
│ │ ├── ExposeResponse.java
│ │ ├── auth/
│ │ │ ├── RegisterRequest.java
│ │ │ ├── RegisterResponse.java
│ │ │ ├── TokenExchangeRequest.java
│ │ │ └── TokenExchangeResponse.java
│ │ └── jwks/
│ │ ├── JwkKey.java
│ │ └── JwksResponse.java
│ ├── tunnel/
│ │ ├── BinaryWsFrame.java
│ │ ├── ControlMessage.java
│ │ ├── HttpTunnelMessage.java
│ │ ├── MessageEnvelope.java
│ │ └── WsTunnelMessage.java
│ └── utils/
│ └── IdUtils.java
├── docker-compose.yml
├── entrypoint-cli-native.sh
├── entrypoint-web.sh
├── entrypoint.sh
├── eureka/
│ ├── HELP.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── tech/
│ │ │ └── amak/
│ │ │ └── portbuddy/
│ │ │ └── eureka/
│ │ │ ├── EurekaApplication.java
│ │ │ └── security/
│ │ │ └── SecurityConfig.java
│ │ └── resources/
│ │ └── application.yml
│ └── test/
│ └── java/
│ └── tech/
│ └── amak/
│ └── portbuddy/
│ └── eureka/
│ └── EurekaApplicationTests.java
├── gateway/
│ ├── HELP.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── tech/
│ │ │ └── amak/
│ │ │ └── portbuddy/
│ │ │ └── gateway/
│ │ │ ├── ApiGatewayApplication.java
│ │ │ ├── client/
│ │ │ │ └── SslServiceClient.java
│ │ │ ├── config/
│ │ │ │ ├── AppProperties.java
│ │ │ │ ├── GlobalExceptionHandler.java
│ │ │ │ ├── LoadBalancerClientsConfig.java
│ │ │ │ ├── NetProxyLoadBalancerConfiguration.java
│ │ │ │ ├── PortBuddyServerLoadBalancerConfiguration.java
│ │ │ │ ├── SslServerConfig.java
│ │ │ │ └── WebClientConfig.java
│ │ │ ├── dto/
│ │ │ │ └── CertificateResponse.java
│ │ │ ├── filter/
│ │ │ │ └── PortBuddyRewritePathGatewayFilterFactory.java
│ │ │ ├── loadbalancer/
│ │ │ │ ├── NetProxyPublicHostLoadBalancer.java
│ │ │ │ └── PortBuddySubdomainLoadBalancer.java
│ │ │ ├── security/
│ │ │ │ ├── GatewayJwtConfig.java
│ │ │ │ └── GatewaySecurityConfig.java
│ │ │ └── ssl/
│ │ │ ├── DynamicSslProvider.java
│ │ │ └── SniSslContextMapping.java
│ │ └── resources/
│ │ └── application.yml
│ └── test/
│ └── java/
│ └── tech/
│ └── amak/
│ ├── gateway/
│ │ └── ApiGatewayApplicationTests.java
│ └── portbuddy/
│ └── gateway/
│ ├── config/
│ │ └── SslServerConfigTest.java
│ └── ssl/
│ └── DynamicSslProviderTest.java
├── lombok.config
├── mvnw
├── mvnw.cmd
├── net-proxy/
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── tech/
│ │ │ └── amak/
│ │ │ └── portbuddy/
│ │ │ └── netproxy/
│ │ │ ├── NetProxyApplication.java
│ │ │ ├── config/
│ │ │ │ ├── AppProperties.java
│ │ │ │ ├── JwtConfig.java
│ │ │ │ └── WebSocketConfig.java
│ │ │ ├── security/
│ │ │ │ └── SecurityConfig.java
│ │ │ ├── tunnel/
│ │ │ │ ├── NetTunnelRegistry.java
│ │ │ │ └── NetTunnelWebSocketHandler.java
│ │ │ └── web/
│ │ │ └── NetProxyController.java
│ │ └── resources/
│ │ └── application.yml
│ └── test/
│ └── java/
│ └── tech/
│ └── amak/
│ └── portbuddy/
│ └── netproxy/
│ └── tunnel/
│ ├── NetTunnelLeakVerificationTest.java
│ ├── NetTunnelOrphanCleanupTest.java
│ ├── NetTunnelRegistryConcurrencyTest.java
│ ├── NetTunnelRegistryTest.java
│ └── NetTunnelUdpEvictionTest.java
├── pom.xml
├── server/
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── tech/
│ │ │ └── amak/
│ │ │ └── portbuddy/
│ │ │ └── server/
│ │ │ ├── ServerApplication.java
│ │ │ ├── client/
│ │ │ │ ├── NetProxyClient.java
│ │ │ │ └── SslServiceClient.java
│ │ │ ├── config/
│ │ │ │ ├── AppProperties.java
│ │ │ │ ├── SchedulingConfig.java
│ │ │ │ ├── ThreatFoxProperties.java
│ │ │ │ └── TunnelsProperties.java
│ │ │ ├── db/
│ │ │ │ ├── entity/
│ │ │ │ │ ├── AccountEntity.java
│ │ │ │ │ ├── ApiKeyEntity.java
│ │ │ │ │ ├── DomainEntity.java
│ │ │ │ │ ├── InvitationEntity.java
│ │ │ │ │ ├── PasswordResetTokenEntity.java
│ │ │ │ │ ├── PortReservationEntity.java
│ │ │ │ │ ├── Role.java
│ │ │ │ │ ├── StripeEventEntity.java
│ │ │ │ │ ├── TunnelEntity.java
│ │ │ │ │ ├── TunnelStatus.java
│ │ │ │ │ ├── UserAccountEntity.java
│ │ │ │ │ └── UserEntity.java
│ │ │ │ └── repo/
│ │ │ │ ├── AccountRepository.java
│ │ │ │ ├── ApiKeyRepository.java
│ │ │ │ ├── DomainRepository.java
│ │ │ │ ├── InvitationRepository.java
│ │ │ │ ├── PasswordResetTokenRepository.java
│ │ │ │ ├── PortReservationRepository.java
│ │ │ │ ├── StripeEventRepository.java
│ │ │ │ ├── TunnelRepository.java
│ │ │ │ ├── UserAccountRepository.java
│ │ │ │ └── UserRepository.java
│ │ │ ├── mail/
│ │ │ │ ├── EmailService.java
│ │ │ │ ├── UserCreatedEvent.java
│ │ │ │ └── WelcomeEmailService.java
│ │ │ ├── security/
│ │ │ │ ├── ApiTokenAuthFilter.java
│ │ │ │ ├── JwtConfig.java
│ │ │ │ ├── JwtService.java
│ │ │ │ ├── Oauth2SuccessHandler.java
│ │ │ │ ├── RsaKeyProvider.java
│ │ │ │ ├── SecurityConfig.java
│ │ │ │ └── ThreatBlockedException.java
│ │ │ ├── service/
│ │ │ │ ├── ApiTokenService.java
│ │ │ │ ├── DomainService.java
│ │ │ │ ├── PaymentCleanupService.java
│ │ │ │ ├── PortReservationService.java
│ │ │ │ ├── ProxyDiscoveryService.java
│ │ │ │ ├── StaleTunnelsReaper.java
│ │ │ │ ├── StripeService.java
│ │ │ │ ├── StripeWebhookService.java
│ │ │ │ ├── TeamService.java
│ │ │ │ ├── TunnelService.java
│ │ │ │ ├── threatfox/
│ │ │ │ │ ├── ThreatFoxClient.java
│ │ │ │ │ ├── ThreatFoxIoc.java
│ │ │ │ │ ├── ThreatFoxRequest.java
│ │ │ │ │ ├── ThreatFoxResponse.java
│ │ │ │ │ └── ThreatFoxService.java
│ │ │ │ └── user/
│ │ │ │ ├── MissingEmailException.java
│ │ │ │ ├── PasswordResetService.java
│ │ │ │ └── UserProvisioningService.java
│ │ │ ├── tunnel/
│ │ │ │ ├── PermissiveSubprotocolHandshakeHandler.java
│ │ │ │ ├── PublicWebSocketProxyHandler.java
│ │ │ │ ├── TunnelRegistry.java
│ │ │ │ ├── TunnelWebSocketHandler.java
│ │ │ │ └── WebSocketConfig.java
│ │ │ └── web/
│ │ │ ├── AuthController.java
│ │ │ ├── DomainsController.java
│ │ │ ├── ExposeController.java
│ │ │ ├── IngressController.java
│ │ │ ├── IngressResolveController.java
│ │ │ ├── InternalDomainController.java
│ │ │ ├── InternalEmailController.java
│ │ │ ├── JwksController.java
│ │ │ ├── PaymentController.java
│ │ │ ├── PortsController.java
│ │ │ ├── StripeWebhookController.java
│ │ │ ├── TeamController.java
│ │ │ ├── TokensController.java
│ │ │ ├── TunnelStatusController.java
│ │ │ ├── TunnelsController.java
│ │ │ ├── UsersController.java
│ │ │ ├── admin/
│ │ │ │ ├── AdminAccountController.java
│ │ │ │ ├── AdminSystemController.java
│ │ │ │ ├── AdminTunnelController.java
│ │ │ │ ├── AdminUserController.java
│ │ │ │ └── dto/
│ │ │ │ ├── AdminAccountRow.java
│ │ │ │ ├── AdminStatsRow.java
│ │ │ │ ├── AdminTunnelRow.java
│ │ │ │ ├── AdminUserRow.java
│ │ │ │ └── SystemStatsResponse.java
│ │ │ ├── advice/
│ │ │ │ └── GlobalExceptionHandler.java
│ │ │ └── dto/
│ │ │ ├── DomainDto.java
│ │ │ ├── LoginRequest.java
│ │ │ ├── PasswordResetConfirm.java
│ │ │ ├── PasswordResetRequest.java
│ │ │ ├── PortRangeDto.java
│ │ │ ├── PortReservationDto.java
│ │ │ ├── PortReservationUpdateRequest.java
│ │ │ ├── SetPasscodeRequest.java
│ │ │ ├── UpdateCustomDomainRequest.java
│ │ │ └── UpdateDomainRequest.java
│ │ └── resources/
│ │ ├── application.yml
│ │ ├── db/
│ │ │ └── migration/
│ │ │ ├── V10__link_http_tunnels_to_domain.sql
│ │ │ ├── V11__add_deleted_column_to_domains.sql
│ │ │ ├── V12__drop_tunnel_id_from_tunnels.sql
│ │ │ ├── V13__password_reset_tokens.sql
│ │ │ ├── V14__port_reservations.sql
│ │ │ ├── V15__add_user_to_port_reservations.sql
│ │ │ ├── V16__add_port_reservation_to_tunnels.sql
│ │ │ ├── V17__soft_delete_port_reservations.sql
│ │ │ ├── V18__add_passcode_to_domains.sql
│ │ │ ├── V19__add_temp_passcode_to_tunnels.sql
│ │ │ ├── V1__accounts_and_users.sql
│ │ │ ├── V20__add_custom_domain_to_domains.sql
│ │ │ ├── V21__add_roles_to_users.sql
│ │ │ ├── V22__update_plans.sql
│ │ │ ├── V23__add_stripe_fields.sql
│ │ │ ├── V24__create_stripe_events_table.sql
│ │ │ ├── V25__create_invitations_table.sql
│ │ │ ├── V26__many_to_many_users_accounts.sql
│ │ │ ├── V27__add_ssl_active_to_domains.sql
│ │ │ ├── V28__add_blocked_to_accounts.sql
│ │ │ ├── V29__add_name_to_port_reservations.sql
│ │ │ ├── V2__api_keys.sql
│ │ │ ├── V30__unique_port_reservation_name.sql
│ │ │ ├── V31__lowercase_domains.sql
│ │ │ ├── V3__tunnels.sql
│ │ │ ├── V4__shedlock_and_heartbeat_indexes.sql
│ │ │ ├── V5__add_password_and_admin.sql
│ │ │ ├── V6__create_domains_table.sql
│ │ │ ├── V7__link_tunnels_to_account.sql
│ │ │ ├── V8__add_user_id_to_tunnels.sql
│ │ │ └── V9__link_api_keys_to_account.sql
│ │ ├── keys/
│ │ │ ├── dev_jwt.pem
│ │ │ └── dev_jwt.pub
│ │ └── templates/
│ │ └── email/
│ │ ├── base.html
│ │ ├── dns-instructions.html
│ │ ├── password-reset-success.html
│ │ ├── password-reset.html
│ │ ├── payment-failed.html
│ │ ├── plan-changed.html
│ │ ├── subscription-canceled.html
│ │ ├── subscription-success.html
│ │ ├── team-invite.html
│ │ └── welcome.html
│ └── test/
│ └── java/
│ └── tech/
│ └── amak/
│ └── portbuddy/
│ └── server/
│ ├── security/
│ │ └── Oauth2SuccessHandlerTest.java
│ ├── service/
│ │ ├── DomainServiceTest.java
│ │ ├── PaymentCleanupServiceTest.java
│ │ ├── PortReservationServiceTest.java
│ │ ├── StaleTunnelsReaperTest.java
│ │ └── TunnelServiceTest.java
│ ├── tunnel/
│ │ └── TunnelRegistryLeakTest.java
│ ├── user/
│ │ └── PasswordResetServiceTest.java
│ └── web/
│ ├── AuthControllerTest.java
│ ├── IngressControllerTest.java
│ ├── PaymentControllerTest.java
│ ├── StripeWebhookControllerTest.java
│ ├── TeamControllerTest.java
│ ├── TokensControllerTest.java
│ ├── UsersControllerTest.java
│ └── admin/
│ └── AdminAccountControllerTest.java
├── ssl-service/
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── tech/
│ │ │ └── amak/
│ │ │ └── portbuddy/
│ │ │ └── sslservice/
│ │ │ ├── SslServiceApplication.java
│ │ │ ├── client/
│ │ │ │ └── ServerClient.java
│ │ │ ├── config/
│ │ │ │ ├── AppProperties.java
│ │ │ │ ├── AsyncConfig.java
│ │ │ │ ├── JpaAuditingConfig.java
│ │ │ │ ├── RestConfig.java
│ │ │ │ └── SchedulingConfig.java
│ │ │ ├── domain/
│ │ │ │ ├── CertificateEntity.java
│ │ │ │ ├── CertificateJobEntity.java
│ │ │ │ ├── CertificateJobStatus.java
│ │ │ │ └── CertificateStatus.java
│ │ │ ├── repo/
│ │ │ │ ├── CertificateJobRepository.java
│ │ │ │ └── CertificateRepository.java
│ │ │ ├── security/
│ │ │ │ └── SecurityConfig.java
│ │ │ ├── service/
│ │ │ │ ├── AcmeAccountService.java
│ │ │ │ ├── AcmeCertificateService.java
│ │ │ │ ├── AcmeClientService.java
│ │ │ │ ├── CertificateStorageService.java
│ │ │ │ ├── DnsResolverService.java
│ │ │ │ ├── EmailService.java
│ │ │ │ ├── RenewalScheduler.java
│ │ │ │ ├── RetryExecutor.java
│ │ │ │ ├── TransientErrorClassifier.java
│ │ │ │ └── impl/
│ │ │ │ ├── ServerEmailService.java
│ │ │ │ └── SimpleDnsResolverService.java
│ │ │ ├── web/
│ │ │ │ ├── CertificatesController.java
│ │ │ │ ├── ChallengeController.java
│ │ │ │ ├── InternalController.java
│ │ │ │ ├── JobsController.java
│ │ │ │ └── dto/
│ │ │ │ ├── CreateCertificateRequest.java
│ │ │ │ ├── CreateManagedCertificateRequest.java
│ │ │ │ └── CreateRootDomainRequest.java
│ │ │ └── work/
│ │ │ └── ChallengeTokenStore.java
│ │ └── resources/
│ │ ├── application.yml
│ │ └── db/
│ │ └── migration/
│ │ ├── V1__init.sql
│ │ ├── V2__shedlock_table.sql
│ │ └── V3__add_full_chain_path.sql
│ └── test/
│ ├── java/
│ │ └── tech/
│ │ └── amak/
│ │ └── portbuddy/
│ │ └── sslservice/
│ │ └── service/
│ │ └── CertificateRenewalServiceTest.java
│ └── resources/
│ └── application-test.yml
└── web/
├── index.html
├── package.json
├── pom.xml
├── postcss.config.js
├── public/
│ ├── install.ps1
│ ├── install.sh
│ ├── pages/
│ │ ├── contacts.html
│ │ ├── docs/
│ │ │ └── guides/
│ │ │ ├── hytale-server.html
│ │ │ └── minecraft-server.html
│ │ ├── docs.html
│ │ ├── index.html
│ │ ├── install.html
│ │ ├── privacy.html
│ │ └── terms.html
│ ├── robots.txt
│ ├── setup-portbuddy-service.ps1
│ ├── setup-portbuddy-service.sh
│ ├── site.webmanifest
│ └── sitemap.xml
├── src/
│ ├── App.tsx
│ ├── auth/
│ │ └── AuthContext.tsx
│ ├── components/
│ │ ├── AppLayout.tsx
│ │ ├── CodeBlock.tsx
│ │ ├── LoadingContext.tsx
│ │ ├── Modal.tsx
│ │ ├── PageHeader.tsx
│ │ ├── PlanComparison.tsx
│ │ ├── ProgressBar.tsx
│ │ └── ProtectedRoute.tsx
│ ├── index.css
│ ├── lib/
│ │ ├── api.ts
│ │ └── utils.ts
│ ├── main.tsx
│ └── pages/
│ ├── AcceptInvite.tsx
│ ├── Contacts.tsx
│ ├── ForgotPassword.tsx
│ ├── Installation.tsx
│ ├── Landing.tsx
│ ├── Login.tsx
│ ├── NotFound.tsx
│ ├── Passcode.tsx
│ ├── Privacy.tsx
│ ├── Register.tsx
│ ├── ResetPassword.tsx
│ ├── ServerError.tsx
│ ├── Terms.tsx
│ ├── app/
│ │ ├── AdminAccounts.tsx
│ │ ├── AdminPanel.tsx
│ │ ├── AdminTunnels.tsx
│ │ ├── AdminUsers.tsx
│ │ ├── Billing.tsx
│ │ ├── BillingCancel.tsx
│ │ ├── BillingSuccess.tsx
│ │ ├── Domains.tsx
│ │ ├── Ports.tsx
│ │ ├── Profile.tsx
│ │ ├── Settings.tsx
│ │ ├── Team.tsx
│ │ ├── Tokens.tsx
│ │ └── Tunnels.tsx
│ └── docs/
│ ├── DocsLayout.tsx
│ ├── DocsOverview.tsx
│ └── guides/
│ ├── HytaleGuide.tsx
│ └── MinecraftGuide.tsx
├── tailwind.config.js
├── tsconfig.json
└── vite.config.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/ci.yml
================================================
name: Build and Deploy
on:
push:
branches: [ "**" ]
tags:
- 'v*'
- 'be*'
- 'fe*'
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Set environment variables based on branch
run: |
echo "ENV=dev" >> $GITHUB_ENV
echo "VITE_API_BASE=http://localhost:8080" >> $GITHUB_ENV
echo "VITE_CANONICAL=http://localhost:8080" >> $GITHUB_ENV
echo "VITE_OG_URL=http://localhost:8080" >> $GITHUB_ENV
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 25
uses: actions/setup-java@v4
with:
java-version: '25'
distribution: 'temurin'
cache: 'maven'
- name: Build with Maven
run: ./mvnw clean package
build-backend:
name: Build and Push Docker Images to YC CR
needs: build
if: startsWith(github.ref, 'refs/tags/be') || startsWith(github.ref, 'refs/tags/fe')
runs-on: ubuntu-latest
steps:
- name: Set environment variables based on branch
run: |
echo "ENV=prod" >> $GITHUB_ENV
echo "VITE_API_BASE=https://portbuddy.dev" >> $GITHUB_ENV
echo "VITE_CANONICAL=https://portbuddy.dev" >> $GITHUB_ENV
echo "VITE_OG_URL=https://portbuddy.dev" >> $GITHUB_ENV
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 25
uses: actions/setup-java@v4
with:
java-version: '25'
distribution: 'temurin'
cache: 'maven'
- name: Build with Maven
run: ./mvnw clean package -pl web -pl eureka -pl gateway -pl net-proxy -pl server -pl ssl-service -am -DskipTests
- name: Login to Yandex Cloud Container Registry
id: login-cr
uses: yc-actions/yc-cr-login@v3
with:
yc-sa-json-credentials: ${{ secrets.YC_CR_JSON_CREDS }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push Server
uses: docker/build-push-action@v6
if: startsWith(github.ref, 'refs/tags/be')
with:
context: .
file: Dockerfile-server
platforms: linux/amd64
push: true
tags: cr.yandex/${{ vars.CR_REGISTRY }}/port-buddy-server:latest
- name: Build and push Gateway
uses: docker/build-push-action@v6
if: startsWith(github.ref, 'refs/tags/be')
with:
context: .
file: Dockerfile-gateway
platforms: linux/amd64
push: true
tags: cr.yandex/${{ vars.CR_REGISTRY }}/port-buddy-gateway:latest
- name: Build and push Eureka
uses: docker/build-push-action@v6
if: startsWith(github.ref, 'refs/tags/be')
with:
context: .
file: Dockerfile-eureka
platforms: linux/amd64
push: true
tags: cr.yandex/${{ vars.CR_REGISTRY }}/port-buddy-eureka:latest
- name: Build and push Net Proxy
uses: docker/build-push-action@v6
if: startsWith(github.ref, 'refs/tags/be')
with:
context: .
file: Dockerfile-net-proxy
platforms: linux/amd64
push: true
tags: cr.yandex/${{ vars.CR_REGISTRY }}/port-buddy-net-proxy:latest
- name: Build and push SSL Service
uses: docker/build-push-action@v6
if: startsWith(github.ref, 'refs/tags/be')
with:
context: .
file: Dockerfile-ssl-service
platforms: linux/amd64
push: true
tags: cr.yandex/${{ vars.CR_REGISTRY }}/port-buddy-ssl-service:latest
- name: Build and push Web App
uses: docker/build-push-action@v6
if: startsWith(github.ref, 'refs/tags/fe')
with:
context: .
file: Dockerfile-web
platforms: linux/amd64
push: true
tags: cr.yandex/${{ vars.CR_REGISTRY }}/port-buddy-web-app:latest
build-portbuddy-docker:
name: Build and Push CLI Images
needs: build
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Extract version
id: extract_version
run: |
if [[ $GITHUB_REF == refs/tags/v* ]]; then
echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
else
echo "VERSION=latest" >> $GITHUB_OUTPUT
fi
- name: Set up JDK 25
uses: actions/setup-java@v4
with:
java-version: '25'
distribution: 'temurin'
cache: 'maven'
- name: Build with Maven
run: ./mvnw clean package -pl cli -am -DskipTests
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push CLI
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile-cli
platforms: linux/amd64,linux/arm64
push: true
tags: |
portbuddy/portbuddy:${{ steps.extract_version.outputs.VERSION }}
portbuddy/portbuddy:latest
build-portbuddy-native:
name: Build Native CLI (${{ matrix.os }})
needs: build
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
asset_name: portbuddy-linux-x64
executable_path: cli/target/portbuddy
- os: ubuntu-24.04-arm
asset_name: portbuddy-linux-arm64
executable_path: cli/target/portbuddy
- os: macos-15-intel
asset_name: portbuddy-macos-x64
executable_path: cli/target/portbuddy
- os: macos-latest
asset_name: portbuddy-macos-arm64
executable_path: cli/target/portbuddy
- os: windows-latest
asset_name: portbuddy-windows-x64.exe
executable_path: cli/target/portbuddy.exe
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Extract version
id: extract_version
shell: bash
run: |
if [[ $GITHUB_REF == refs/tags/v* ]]; then
echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
else
echo "VERSION=latest" >> $GITHUB_OUTPUT
fi
- name: Set up MSVC
if: matrix.os == 'windows-latest'
uses: ilammy/msvc-dev-cmd@v1
- name: Set up GraalVM
uses: actions/setup-java@v4
with:
java-version: '25'
distribution: 'graalvm'
cache: 'maven'
- name: Build with Maven (Native Image)
run: ./mvnw clean package -pl cli -am -DskipTests -Pnative
- name: Prepare Artifact
id: prepare_artifact
shell: bash
run: |
cp ${{ matrix.executable_path }} ${{ matrix.asset_name }}
if command -v shasum &> /dev/null; then
SHA=$(shasum -a 256 ${{ matrix.asset_name }} | awk '{print $1}')
elif command -v sha256sum &> /dev/null; then
SHA=$(sha256sum ${{ matrix.asset_name }} | awk '{print $1}')
else
echo "No shasum or sha256sum found"
exit 1
fi
echo "sha256=$SHA" >> $GITHUB_OUTPUT
- name: Release
uses: softprops/action-gh-release@v2
with:
files: ${{ matrix.asset_name }}
tag_name: ${{ steps.extract_version.outputs.VERSION }}
name: Release ${{ steps.extract_version.outputs.VERSION }}
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
update-homebrew:
name: Update Homebrew Tap
needs: build-portbuddy-native
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
steps:
- name: Checkout Homebrew Tap
uses: actions/checkout@v4
with:
repository: amak-tech/homebrew-tap # REPLACE with your actual tap repository
token: ${{ secrets.HOMEBREW_TAP_TOKEN }} # Needs a PAT with 'repo' scope
- name: Extract version
id: extract_version
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
- name: Update Formula
run: |
# Use values from build-native-cli outputs
# Note: Accessing matrix outputs by job ID is complex.
# An easier way is to download them from the release if they are already there.
# Or since we are in the same workflow, we can use 'gh release download'
gh release download ${{ steps.extract_version.outputs.VERSION }} -p "portbuddy-*" --repo ${{ github.repository }}
if command -v shasum &> /dev/null; then
MAC_ARM_SHA=$(shasum -a 256 portbuddy-macos-arm64 | awk '{print $1}')
MAC_X64_SHA=$(shasum -a 256 portbuddy-macos-x64 | awk '{print $1}')
LINUX_ARM_SHA=$(shasum -a 256 portbuddy-linux-arm64 | awk '{print $1}')
LINUX_X64_SHA=$(shasum -a 256 portbuddy-linux-x64 | awk '{print $1}')
else
MAC_ARM_SHA=$(sha256sum portbuddy-macos-arm64 | awk '{print $1}')
MAC_X64_SHA=$(sha256sum portbuddy-macos-x64 | awk '{print $1}')
LINUX_ARM_SHA=$(sha256sum portbuddy-linux-arm64 | awk '{print $1}')
LINUX_X64_SHA=$(sha256sum portbuddy-linux-x64 | awk '{print $1}')
fi
sed -i "s/version \".*\"/version \"${{ steps.extract_version.outputs.VERSION }}\"/" Formula/portbuddy.rb
sed -i "/portbuddy-macos-arm64\"/{n;s/sha256 \".*\"/sha256 \"$MAC_ARM_SHA\"/;}" Formula/portbuddy.rb
sed -i "/portbuddy-macos-x64\"/{n;s/sha256 \".*\"/sha256 \"$MAC_X64_SHA\"/;}" Formula/portbuddy.rb
sed -i "/portbuddy-linux-arm64\"/{n;s/sha256 \".*\"/sha256 \"$LINUX_ARM_SHA\"/;}" Formula/portbuddy.rb
sed -i "/portbuddy-linux-x64\"/{n;s/sha256 \".*\"/sha256 \"$LINUX_X64_SHA\"/;}" Formula/portbuddy.rb
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Commit and Push
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Formula/portbuddy.rb
git commit -m "Update portbuddy to v${{ steps.extract_version.outputs.VERSION }}"
git push
================================================
FILE: .gitignore
================================================
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
.kotlin
### IntelliJ IDEA ###
.idea/*
.idea
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store
node_modules/
dist
.env
ref
pg_data
log
/checkstyle_report.txt
/test_output.txt
/config/acme/account.key
/ssl-service/config/acme/account.key
.junie
================================================
FILE: .mvn/wrapper/maven-wrapper.properties
================================================
wrapperVersion=3.3.4
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
================================================
FILE: Dockerfile-cli
================================================
FROM eclipse-temurin:25-jre
WORKDIR /app
COPY cli/target/cli-*.jar /app/app.jar
COPY entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
ENV JVM_OPTS="--enable-native-access=ALL-UNNAMED"
ENTRYPOINT ["/app/entrypoint.sh"]
================================================
FILE: Dockerfile-eureka
================================================
FROM eclipse-temurin:25-jre
WORKDIR /app
COPY eureka/target/eureka-*.jar /app/app.jar
COPY entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
ENTRYPOINT ["/app/entrypoint.sh"]
================================================
FILE: Dockerfile-gateway
================================================
FROM eclipse-temurin:25-jre
WORKDIR /app
COPY gateway/target/gateway-*.jar /app/app.jar
COPY entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
ENTRYPOINT ["/app/entrypoint.sh"]
================================================
FILE: Dockerfile-net-proxy
================================================
FROM eclipse-temurin:25-jre
WORKDIR /app
COPY net-proxy/target/net-proxy-*.jar /app/app.jar
COPY entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
ENTRYPOINT ["/app/entrypoint.sh"]
================================================
FILE: Dockerfile-server
================================================
FROM eclipse-temurin:25-jre
WORKDIR /app
COPY server/target/server-*.jar /app/app.jar
COPY entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
ENTRYPOINT ["/app/entrypoint.sh"]
================================================
FILE: Dockerfile-ssl-service
================================================
FROM eclipse-temurin:25-jre
WORKDIR /app
COPY ssl-service/target/ssl-service-*.jar /app/app.jar
COPY entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
ENTRYPOINT ["/app/entrypoint.sh"]
================================================
FILE: Dockerfile-web
================================================
FROM alpine:latest
WORKDIR /app
COPY web/dist /app/dist
COPY entrypoint-web.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
ENTRYPOINT ["/app/entrypoint.sh"]
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
# PortBuddy 🚀
PortBuddy is a powerful yet simple tool that allows you to expose a port opened on your local host or in a private network to the public internet. It works as a secure tunnel, similar to ngrok, providing a public URL for your local services.
Whether you're developing a web app, testing webhooks, or sharing access to a local database, PortBuddy makes it easy and secure.
## ✨ Features
- **Multi-protocol support**: Tunnel HTTP, TCP, and UDP traffic.
- **SSL by default**: All HTTP tunnels are automatically secured with SSL.
- **Customizable**: Support for static subdomains and custom domains.
- **Websocket support**: Full support for real-time applications.
- **Private tunnels**: Secure your tunnels with passcodes.
- **Cross-platform CLI**: Lightweight CLI built with Java 25 and GraalVM (native executable).
- **Web Dashboard**: Manage your tunnels, subscriptions, and team members easily.
## 🚀 Quick Start
### 1. Installation
Download the latest version of the `portbuddy` CLI for your platform (Windows, Linux, or Mac).
### 2. Authentication
Before exposing ports, you need to authenticate your CLI.
1. Log in to your account at [portbuddy.dev](https://portbuddy.dev).
2. Generate an API Token in your dashboard.
3. Run the following command:
```bash
portbuddy init {YOUR_API_TOKEN}
```
### 3. Expose a Port
#### HTTP (Default)
Expose a local web server running on port 3000:
```bash
portbuddy 3000
```
Output: `http://localhost:3000 exposed to: https://abc123.portbuddy.dev`
#### TCP
Expose a local PostgreSQL database:
```bash
portbuddy tcp 5432
```
Output: `tcp localhost:5432 exposed to: net-proxy-3.portbuddy.dev:43452`
#### UDP
Expose a local UDP service:
```bash
portbuddy udp 9000
```
### 4. Run with Docker
You can also run the PortBuddy CLI inside a Docker container.
#### Pull the Image
If you haven't built it locally, you can use the official image (replace with actual image name if applicable):
```bash
docker pull amaktech/portbuddy:latest
```
*Note: If you are developing locally, you can build the image using `Dockerfile-cli`.*
#### Authentication with Docker
To use PortBuddy in Docker, you should mount your token file from your host machine to the container. The CLI expects the token at `/root/.port-buddy/token`.
First, authenticate on your host machine:
```bash
portbuddy init {YOUR_API_TOKEN}
```
Then, run the Docker container with the token mounted:
```bash
docker run -it --rm \
-v ~/.port-buddy/token:/root/.port-buddy/token \
--network host \
amaktech/portbuddy 3000
```
#### Mounting a Token File Directly
If you have your API token in a file (e.g., `my_token.txt`), you can mount it directly:
```bash
docker run -it --rm \
-v $(pwd)/my_token.txt:/root/.port-buddy/token \
--network host \
amaktech/portbuddy 3000
```
*Note: `--network host` is used to allow the container to access services running on your host's localhost (e.g., port 3000).*
## 🛠️ CLI Usage
```text
Usage: portbuddy [options] [mode] [host:][port]
Modes:
http (default), tcp, udp
Options:
-d, --domain=<domain> Requested static subdomain (e.g. my-app)
-pr, --port-reservation=<hp> Use specific port reservation host:port for TCP/UDP
-pc, --passcode=<passcode> Protect tunnel with a passcode
-v, --verbose Enable verbose logging
-h, --help Show help message
-V, --version Show version info
```
## 💳 Subscription Plans
| Feature | Pro ($0/mo) | Team ($10/mo) |
| :--- | :--- | :--- |
| Tunnels | HTTP, TCP, UDP | Everything in Pro |
| SSL | Included | Included |
| Subdomains | Static | Static |
| Custom Domains | Supported | Supported |
| Team Members | - | Included |
| Free Tunnels | 1 at a time | 10 at a time |
| Extra Tunnels | $1/mo each | $1/mo each |
| Support | Standard | Priority |
## 🏗️ Architecture
PortBuddy is built as a multi-modular system:
- **`cli`**: GraalVM-native command-line application (Java 25).
- **`server`**: Spring Boot 3.5.7 API & Tunnel Management.
- **`net-proxy`**: High-performance TCP/UDP proxy.
- **`gateway`**: Webflux-based API Gateway.
- **`web`**: React-based dashboard (TypeScript + TailwindCSS).
- **`eureka`**: Service discovery.
- **`ssl-service`**: Automated SSL certificate management.
- **`common`**: Shared DTOs and utilities.
## 🛠️ Development
### Prerequisites
- Java 25
- Docker & Docker Compose
- Spring Boot 3
- Maven 3.9+
- Node.js & npm (for web module)
### Build
To build the entire project:
```bash
./mvnw clean install
```
### Run with Docker Compose
```bash
docker-compose up -d
```
## 📄 License
This project is licensed under the Apache License, Version 2.0 - see the [LICENSE](LICENSE) file for details.
## 🤝 Community Projects
Projects built by the PortBuddy community:
- **[PortBuddy GUI](https://github.com/quack-stuff/portbuddy-gui)** by Quack - A graphical user interface for PortBuddy on Windows.
================================================
FILE: cli/pom.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>tech.amak</groupId>
<artifactId>port-buddy</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>cli</artifactId>
<name>port-buddy-cli</name>
<properties>
<maven.compiler.release>25</maven.compiler.release>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.16</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.5.16</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.18.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>2.18.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.18.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.18.1</version>
</dependency>
<dependency>
<groupId>tech.amak</groupId>
<artifactId>common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<dependency>
<groupId>org.jline</groupId>
<artifactId>jline</artifactId>
<version>3.26.3</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.11.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.11.4</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.4.2</version>
<configuration>
<archive>
<manifest>
<mainClass>tech.amak.portbuddy.cli.PortBuddy</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>tech.amak.portbuddy.cli.PortBuddy</mainClass>
<manifestEntries>
<Implementation-Title>${project.name}</Implementation-Title>
<Implementation-Version>${project.version}</Implementation-Version>
</manifestEntries>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>native</id>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>0.11.3</version>
<extensions>true</extensions>
<executions>
<execution>
<id>build-native</id>
<goals>
<goal>compile-no-fork</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
<configuration>
<imageName>portbuddy</imageName>
<mainClass>tech.amak.portbuddy.cli.PortBuddy</mainClass>
<debug>false</debug>
<verbose>false</verbose>
<buildArgs>
<buildArg>-Os</buildArg>
<buildArg>--no-fallback</buildArg>
<buildArg>--enable-http</buildArg>
<buildArg>--enable-https</buildArg>
<buildArg>--gc=serial</buildArg>
<buildArg>-H:+UnlockExperimentalVMOptions</buildArg>
<buildArg>-H:-StackTrace</buildArg>
<buildArg>--initialize-at-build-time=org.slf4j,ch.qos.logback,tech.amak.portbuddy.common</buildArg>
</buildArgs>
<jvmArgs>
<jvmArg>--enable-native-access=ALL-UNNAMED</jvmArg>
</jvmArgs>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
================================================
FILE: cli/src/main/java/tech/amak/portbuddy/cli/PortBuddy.java
================================================
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tech.amak.portbuddy.cli;
import static tech.amak.portbuddy.cli.utils.JsonUtils.MAPPER;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import lombok.extern.slf4j.Slf4j;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import tech.amak.portbuddy.cli.config.ConfigurationService;
import tech.amak.portbuddy.cli.tunnel.HttpTunnelClient;
import tech.amak.portbuddy.cli.tunnel.NetTunnelClient;
import tech.amak.portbuddy.cli.ui.ConsoleUi;
import tech.amak.portbuddy.cli.utils.HttpUtils;
import tech.amak.portbuddy.common.ClientConfig;
import tech.amak.portbuddy.common.TunnelType;
import tech.amak.portbuddy.common.dto.ExposeRequest;
import tech.amak.portbuddy.common.dto.ExposeResponse;
import tech.amak.portbuddy.common.dto.auth.RegisterRequest;
import tech.amak.portbuddy.common.dto.auth.RegisterResponse;
import tech.amak.portbuddy.common.dto.auth.TokenExchangeRequest;
import tech.amak.portbuddy.common.dto.auth.TokenExchangeResponse;
/**
* Main class for the PortBuddy CLI application.
* Handles command-line argument parsing and coordinates tunnel exposure.
*/
@Slf4j
public class PortBuddy {
private static final String OUTDATED = "outdated";
private static final int EXIT_OK = 0;
private static final int EXIT_ERROR = 1;
private static final int EXIT_USAGE = 2;
private final ConfigurationService configurationService = ConfigurationService.INSTANCE;
private String domain;
private String portReservation;
private String passcode;
private boolean verbose;
private final List<String> positionalArgs = new ArrayList<>();
private final OkHttpClient http = HttpUtils.createClient();
/**
* Main entry point for the application.
*
* @param args command line arguments
*/
public static void main(final String[] args) {
final var portBuddy = new PortBuddy();
final var exitCode = portBuddy.execute(args);
System.exit(exitCode);
}
/**
* Parses arguments and executes the requested command.
*
* @param args command line arguments
* @return exit code
*/
public int execute(final String[] args) {
if (args.length == 0) {
printHelp();
return EXIT_USAGE;
}
var i = 0;
while (i < args.length) {
final var arg = args[i];
if ("-h".equals(arg) || "--help".equals(arg)) {
printHelp();
return EXIT_OK;
} else if ("-V".equals(arg) || "--version".equals(arg)) {
printVersion();
return EXIT_OK;
} else if ("-v".equals(arg) || "--verbose".equals(arg)) {
this.verbose = true;
} else if ("-d".equals(arg) || "--domain".equals(arg)) {
if (++i < args.length) {
this.domain = args[i];
} else {
System.err.println("Error: Option '-d', '--domain' requires an argument.");
return EXIT_USAGE;
}
} else if (arg.startsWith("--domain=")) {
this.domain = arg.substring("--domain=".length());
} else if ("-pr".equals(arg) || "--port-reservation".equals(arg)) {
if (++i < args.length) {
this.portReservation = args[i];
} else {
System.err.println("Error: Option '-pr', '--port-reservation' requires an argument.");
return EXIT_USAGE;
}
} else if (arg.startsWith("--port-reservation=")) {
this.portReservation = arg.substring("--port-reservation=".length());
} else if ("-pc".equals(arg) || "--passcode".equals(arg)) {
if (++i < args.length) {
this.passcode = args[i];
} else {
System.err.println("Error: Option '-pc', '--passcode' requires an argument.");
return EXIT_USAGE;
}
} else if (arg.startsWith("--passcode=")) {
this.passcode = arg.substring("--passcode=".length());
} else if ("-n".equals(arg) || "--no-request-log".equals(arg)) {
configurationService.getConfig().setLogEnabled(false);
} else if ("init".equals(arg)) {
if (++i < args.length) {
return init(args[i]);
} else {
System.err.println("Error: Missing API token for 'init' command.");
return EXIT_USAGE;
}
} else if (arg.startsWith("-")) {
System.err.println("Unknown option: " + arg);
printHelp();
return EXIT_USAGE;
} else {
positionalArgs.add(arg);
}
i++;
}
return expose();
}
private void printHelp() {
System.out.println("Usage: portbuddy [options] [mode] [host:][port]");
System.out.println("Expose local ports to public network (simple ngrok alternative).");
System.out.println();
System.out.println("Options:");
System.out.println(" -d, --domain=<domain> Requested domain (e.g. my-domain)");
System.out.println(" -pr, --port-reservation=<host:port>");
System.out.println(" Use specific port reservation host:port for TCP/UDP");
System.out.println(" -pc, --passcode=<passcode> Passcode to secure HTTP tunnel (temporary for this tunnel)");
System.out.println(" -n, --no-request-log Disable request logging");
System.out.println(" -v, --verbose Verbose logging");
System.out.println(" -h, --help Show this help message and exit.");
System.out.println(" -V, --version Print version information and exit.");
System.out.println();
System.out.println("Commands:");
System.out.println(" init <apiToken> Initialize CLI with API token");
System.out.println();
System.out.println("Examples:");
System.out.println(" portbuddy 3000");
System.out.println(" portbuddy tcp 5432");
System.out.println(" portbuddy --domain=my-app 8080");
}
private void printVersion() {
System.out.println("portbuddy " + resolveCliVersion());
}
private int init(final String apiToken) {
try {
configurationService.saveApiToken(apiToken);
System.out.println("API token saved. You're now authenticated.");
return EXIT_OK;
} catch (final IOException e) {
System.err.println("Failed to save API token: " + e.getMessage());
return EXIT_ERROR;
}
}
private int expose() {
final String modeStr;
final String hostPortStr;
if (positionalArgs.isEmpty()) {
System.err.println("Usage: portbuddy [mode] [host:][port] or [schema://]host[:port]");
return EXIT_USAGE;
} else if (positionalArgs.size() == 1) {
modeStr = null; // default http
hostPortStr = positionalArgs.get(0);
} else {
modeStr = positionalArgs.get(0);
hostPortStr = positionalArgs.get(1);
}
final var mode = TunnelType.from(modeStr);
final var hostPort = parseHostPort(hostPortStr);
if (hostPort == null) {
return EXIT_USAGE;
}
if (hostPort.port < 1 || hostPort.port > 65535) {
System.err.println("Port must be in range [1, 65535]");
return EXIT_USAGE;
}
final var config = configurationService.getConfig();
// 1) Ensure API key is present and exchange it for a JWT at startup
if (!ensureAuthenticated(config)) {
return EXIT_ERROR;
}
final var apiKey = config.getApiToken();
final var jwt = exchangeApiTokenForJwt(config.getServerUrl(), apiKey);
if (Objects.equals(jwt, OUTDATED)) {
System.err.println("""
Your portbuddy CLI is outdated.
Please upgrade to the latest version and try again.""");
return EXIT_ERROR;
}
if (jwt == null || jwt.isBlank()) {
System.err.println("""
Failed to authenticate with the provided API Key.
CLI must be initialized with a valid API Key.
Example: portbuddy init {API_TOKEN}""");
return EXIT_ERROR;
}
if (mode == TunnelType.HTTP) {
final var expose = callExposeTunnel(config.getServerUrl(), jwt,
new ExposeRequest(mode, hostPort.scheme, hostPort.host, hostPort.port, domain, null, passcode));
if (expose == null) {
System.err.println("Failed to contact server to create tunnel");
return EXIT_ERROR;
}
final var localInfo = String.format("%s://%s:%d", hostPort.scheme, hostPort.host, hostPort.port);
final var publicInfo = expose.publicUrl();
final var ui = new ConsoleUi(TunnelType.HTTP, localInfo, publicInfo);
final var tunnelId = expose.tunnelId();
if (tunnelId == null) {
System.err.println("Server did not return tunnelId");
return EXIT_ERROR;
}
final var client = new HttpTunnelClient(
config.getServerUrl(),
tunnelId,
hostPort.host,
hostPort.port,
hostPort.scheme,
jwt,
publicInfo,
ui,
verbose
);
final var thread = new Thread(client::runBlocking, "port-buddy-http-client");
ui.setOnExit(client::close);
thread.start();
ui.start();
ui.waitForExit();
try {
thread.join(2000);
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
}
} else {
final var scheme = mode == TunnelType.UDP ? "udp" : "tcp";
final var expose = callExposeTunnel(config.getServerUrl(), jwt,
new ExposeRequest(mode, scheme, hostPort.host, hostPort.port, null, portReservation, null));
if (expose == null || expose.publicHost() == null || expose.publicPort() == null) {
System.err.println("Failed to contact server to create " + mode + " tunnel");
return EXIT_ERROR;
}
final var localInfo = String.format("%s %s:%d", mode.name().toLowerCase(), hostPort.host, hostPort.port);
final var publicInfo = String.format("%s:%d", expose.publicHost(), expose.publicPort());
final var ui = new ConsoleUi(mode, localInfo, publicInfo);
final var tunnelId = expose.tunnelId();
if (tunnelId == null) {
System.err.println("Server did not return tunnelId");
return EXIT_ERROR;
}
// Use configured API server URL for the WebSocket control channel, not the public TCP host
final var serverUri = URI.create(config.getServerUrl());
final var wsHost = serverUri.getHost();
final var wsPort = serverUri.getPort() == -1
? ("https".equalsIgnoreCase(serverUri.getScheme()) ? 443 : 80)
: serverUri.getPort();
final var secure = "https".equalsIgnoreCase(serverUri.getScheme());
final var tcpClient = new NetTunnelClient(
wsHost,
wsPort,
secure,
tunnelId,
hostPort.host,
hostPort.port,
mode,
expose.publicHost(),
expose.publicPort(),
jwt,
ui,
verbose);
final var thread = new Thread(tcpClient::runBlocking, "port-buddy-net-client-" + mode.name().toLowerCase());
ui.setOnExit(tcpClient::close);
thread.start();
ui.start();
ui.waitForExit();
try {
thread.join(2000);
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println("\nThanks, bye!");
return EXIT_OK;
}
private ExposeResponse callExposeTunnel(final String baseUrl, final String jwt, final ExposeRequest requestBody) {
final var tunnelType = requestBody.tunnelType();
try {
final var url = baseUrl + "/api/expose/"
+ (tunnelType == TunnelType.HTTP ? "http" : "net");
final var json = MAPPER.writeValueAsString(requestBody);
final var request = new Request.Builder()
.url(url)
.post(RequestBody.create(json, MediaType.parse("application/json")))
.header("Authorization", "Bearer " + jwt)
.build();
try (final var response = http.newCall(request).execute()) {
if (!response.isSuccessful()) {
log.warn("Expose {} failed: {} {}", tunnelType, response.code(), response.message());
if (response.code() == 401) {
System.err.println("Authentication failed. Please re-initialize CLI with a valid API Key.\n"
+ "Example: portbuddy init {API_TOKEN}");
}
return null;
}
final var body = response.body();
if (body == null) {
return null;
}
return MAPPER.readValue(body.string(), ExposeResponse.class);
}
} catch (final Exception e) {
log.warn("Expose {} tunnel call error: {}", tunnelType, e.toString());
return null;
}
}
/**
* Exchanges the provided API token for a JWT using the server's auth endpoint.
* Returns the JWT string if successful, otherwise null.
*/
private String exchangeApiTokenForJwt(final String baseUrl, final String apiToken) {
try {
final var url = baseUrl + "/api/auth/token-exchange";
final var cliVersion = resolveCliVersion();
final var payload = new TokenExchangeRequest(apiToken, cliVersion);
final var json = MAPPER.writeValueAsString(payload);
final var request = new Request.Builder()
.url(url)
.post(RequestBody.create(json, MediaType.parse("application/json")))
.build();
try (final var response = http.newCall(request).execute()) {
if (!response.isSuccessful()) {
log.warn("Token exchange failed: {} {}", response.code(), response.message());
if (response.code() == 426) {
return OUTDATED;
}
return null;
}
final var body = response.body();
if (body == null) {
return null;
}
final var resp = MAPPER.readValue(body.string(), TokenExchangeResponse.class);
final var accessToken = resp.getAccessToken() == null ? "" : resp.getAccessToken();
final var tokenType = resp.getTokenType() == null ? "" : resp.getTokenType();
if (!accessToken.isBlank() && (tokenType.isBlank() || "Bearer".equalsIgnoreCase(tokenType))) {
return accessToken;
}
return null;
}
} catch (final Exception e) {
log.warn("Token exchange call error: {}", e.toString());
return null;
}
}
private String resolveCliVersion() {
final var pkg = PortBuddy.class.getPackage();
final var impl = pkg == null ? null : pkg.getImplementationVersion();
if (impl != null && !impl.isBlank()) {
return impl.trim();
}
return "1.0";
}
private boolean ensureAuthenticated(final ClientConfig config) {
final var apiKey = config.getApiToken();
if (apiKey != null && !apiKey.isBlank()) {
return true;
}
System.out.println("No API key found. Please sign up.");
try {
final var request = ConsoleUi.promptForUserRegistration();
if (request == null) {
System.err.println("Registration cancelled or failed to get details.");
return false;
}
final var serverUrl = config.getServerUrl();
if (serverUrl == null || serverUrl.isBlank()) {
System.err.println("Error: Server URL is not configured. Please check your application.yml.");
return false;
}
final var newApiKey = registerUser(serverUrl, request);
configurationService.saveApiToken(newApiKey);
config.setApiToken(newApiKey);
System.out.println("Registration successful! API key saved.");
return true;
} catch (final Exception e) {
log.debug("Registration failed", e);
System.err.println("Registration failed: " + e.getMessage());
if (e.getMessage() == null) {
System.err.println("Error details: " + e.getClass().getName());
e.printStackTrace(System.err); // Print stack trace to see exactly where NPE happens
}
return false;
}
}
private String registerUser(final String baseUrl,
final RegisterRequest registerRequest) throws IOException {
final var url = baseUrl + (baseUrl.endsWith("/") ? "" : "/") + "api/auth/register";
final var json = MAPPER.writeValueAsString(registerRequest);
final var request = new Request.Builder()
.url(url)
.post(RequestBody.create(json, MediaType.parse("application/json")))
.build();
try (final var response = http.newCall(request).execute()) {
final var respBody = response.body();
if (respBody == null) {
if (!response.isSuccessful()) {
throw new IOException("Server returned: " + response.message());
}
throw new IOException("Empty response from server");
}
if (response.code() == 503) {
throw new IOException("Server is unavailable. Please try again later.");
}
final var bodyStr = respBody.string();
try {
final var registerResponse = MAPPER.readValue(bodyStr, RegisterResponse.class);
if (!registerResponse.isSuccess()) {
throw new IOException(registerResponse.getMessage());
}
return registerResponse.getApiKey();
} catch (final IOException e) {
if (!response.isSuccessful()) {
throw new IOException("Server returned: " + response.message());
}
throw e;
}
}
}
private HostPort parseHostPort(final String arg) {
var scheme = "http"; // default scheme
var host = "localhost"; // default host
Integer port = null;
if (arg == null || arg.isBlank()) {
System.err.println("Missing [host:][port] or [schema://]host[:port]. Example: 'portbuddy 3000' or 'portbuddy https://localhost'");
return null;
}
var url = arg.trim();
if (url.endsWith("/")) {
url = url.substring(0, url.length() - 1);
}
// Case 1: pure port number, e.g. "3000"
if (!url.contains("://") && !url.contains(":")) {
// try parse as number; if fails, treat as host without port
try {
port = Integer.parseInt(url);
return new HostPort(host, port, scheme);
} catch (final NumberFormatException ignore) {
// not a pure number -> host only, keep going
host = url;
port = null;
}
}
var schemeExplicit = false;
// Case 2: URL with scheme: http(s)://host[:port]
if (url.contains("://")) {
final var parts = url.split("://", 2);
final var givenScheme = parts[0].toLowerCase();
if (!givenScheme.equals("http") && !givenScheme.equals("https")) {
System.err.println("Unsupported schema: " + givenScheme + ". Only http or https are allowed.");
return null;
}
scheme = givenScheme;
schemeExplicit = true;
final var rest = parts[1];
if (rest.contains(":")) {
final var hostPortPart = rest.split(":", 2);
host = hostPortPart[0].isBlank() ? host : hostPortPart[0];
try {
port = Integer.parseInt(hostPortPart[1]);
} catch (final NumberFormatException e) {
System.err.println("Invalid port: " + hostPortPart[1]);
return null;
}
} else if (!rest.isBlank()) {
host = rest;
}
} else if (port == null) {
// Case 3: host[:port] (no scheme)
if (url.contains(":")) {
final var hostPortPart = url.split(":", 2);
host = hostPortPart[0].isBlank() ? host : hostPortPart[0];
try {
port = Integer.parseInt(hostPortPart[1]);
} catch (final NumberFormatException e) {
System.err.println("Invalid port: " + hostPortPart[1]);
return null;
}
} else {
host = url;
}
}
if (port == null) {
// Default port by scheme
port = scheme.equals("https") ? 443 : 80;
}
// If scheme is not explicit and port is 443/80, infer scheme from common defaults
if (!schemeExplicit) {
if (port == 443) {
scheme = "https";
} else if (port == 80) {
scheme = "http";
}
}
return new HostPort(host, port, scheme);
}
private static final class HostPort {
private final String host;
private final int port;
private final String scheme;
private HostPort(final String host, final int port, final String scheme) {
this.host = host;
this.port = port;
this.scheme = scheme;
}
}
}
================================================
FILE: cli/src/main/java/tech/amak/portbuddy/cli/config/ConfigurationService.java
================================================
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tech.amak.portbuddy.cli.config;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.util.HashSet;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import lombok.extern.slf4j.Slf4j;
import tech.amak.portbuddy.common.ClientConfig;
@Slf4j
public class ConfigurationService {
private static final String PORT_BUDDY_ENV = "PORT_BUDDY_ENV";
private static final String PORT_BUDDY_ENV_DEV = "dev";
private static final String APP_DIR = ".port-buddy";
private static final String TOKEN_FILE = "token";
private final String home = System.getProperty("user.home");
private final ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory());
private final AtomicReference<ClientConfig> config = new AtomicReference<>();
public static final ConfigurationService INSTANCE = new ConfigurationService();
private ConfigurationService() {
try {
configureLogging();
loadConfig();
loadToken();
} catch (final Exception e) {
log.error("Failed to load config: {}", e.toString());
config.set(new ClientConfig());
}
}
private void configureLogging() {
final var env = System.getenv(PORT_BUDDY_ENV);
final var loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
final var rootLogger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME);
rootLogger.setLevel(Level.ERROR);
if (PORT_BUDDY_ENV_DEV.equalsIgnoreCase(env)) {
final var appLogger = loggerContext.getLogger("tech.amak.portbuddy");
appLogger.setLevel(Level.DEBUG);
}
}
private void loadConfig() throws IOException {
final var envPart = Optional.ofNullable(System.getenv(PORT_BUDDY_ENV))
.map(String::toLowerCase)
.map("-%s"::formatted)
.orElse("");
final var resourceName = "/application%s.yml".formatted(envPart);
log.debug("Loading config from resource: {}", resourceName);
try (final var configStream = ConfigurationService.class.getResourceAsStream(resourceName)) {
if (configStream != null) {
final var clientConfig = yamlMapper.readValue(configStream, ClientConfig.class);
log.debug("Loaded config: {}", clientConfig);
config.set(clientConfig);
} else {
log.warn("Resource not found: {}", resourceName);
if (config.get() == null) {
config.set(new ClientConfig());
}
}
}
}
private void loadToken() throws IOException {
final var tokenFile = Path.of(home, APP_DIR, TOKEN_FILE);
if (Files.exists(tokenFile)) {
final var token = Files.readString(tokenFile).trim();
if (!token.isBlank()) {
config.get().setApiToken(token);
}
}
}
public ClientConfig getConfig() {
return config.get();
}
public boolean isDev() {
final var env = System.getenv(PORT_BUDDY_ENV);
return PORT_BUDDY_ENV_DEV.equalsIgnoreCase(env);
}
/**
* Saves the provided API token to a file located in the user's home directory
* under the ".port-buddy" directory. If the directory or file does not exist,
* they are created. File permissions are restricted to the owner on POSIX systems.
*
* @param token the API token to be saved. It is stripped of leading and trailing whitespaces
* before being written to the file.
* @throws IOException if an I/O error occurs while creating the directory, file, or writing the token.
*/
public void saveApiToken(final String token) throws IOException {
final var dir = Path.of(home, APP_DIR);
if (!Files.exists(dir)) {
Files.createDirectories(dir);
}
final var file = dir.resolve(TOKEN_FILE);
Files.writeString(file, token.strip());
// Try to restrict permissions on POSIX systems
try {
final var perms = new HashSet<PosixFilePermission>();
perms.add(PosixFilePermission.OWNER_READ);
perms.add(PosixFilePermission.OWNER_WRITE);
Files.setPosixFilePermissions(file, perms);
} catch (final UnsupportedOperationException ignore) {
// Non-POSIX filesystem (e.g., Windows) - best effort only
}
}
}
================================================
FILE: cli/src/main/java/tech/amak/portbuddy/cli/tunnel/HttpTunnelClient.java
================================================
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tech.amak.portbuddy.cli.tunnel;
import static tech.amak.portbuddy.cli.utils.JsonUtils.MAPPER;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import okio.ByteString;
import tech.amak.portbuddy.cli.config.ConfigurationService;
import tech.amak.portbuddy.cli.ui.ConsoleUi;
import tech.amak.portbuddy.cli.ui.HttpLogSink;
import tech.amak.portbuddy.cli.utils.HttpUtils;
import tech.amak.portbuddy.common.tunnel.ControlMessage;
import tech.amak.portbuddy.common.tunnel.HttpTunnelMessage;
import tech.amak.portbuddy.common.tunnel.MessageEnvelope;
import tech.amak.portbuddy.common.tunnel.WsTunnelMessage;
@Slf4j
@RequiredArgsConstructor
public class HttpTunnelClient {
private final String serverUrl; // e.g. https://portbuddy.dev
private final UUID tunnelId;
private final String localHost;
private final int localPort;
private final String localScheme; // http or https
private final String authToken; // Bearer token for API auth
private final String publicBaseUrl; // e.g. https://abc123.portbuddy.dev
private final HttpLogSink httpLogSink;
private final boolean verbose;
// OkHttp client used exclusively for the control WebSocket connection to the server
private final OkHttpClient http = createHttpClient();
// Separate OkHttp client for calling the local target service (avoid any interference with WS client)
private final OkHttpClient localHttp = createLocalHttpClient();
private static OkHttpClient createHttpClient() {
final var builder = new OkHttpClient.Builder()
.readTimeout(0, TimeUnit.MILLISECONDS) // keep-alive for WS
.pingInterval(15, TimeUnit.SECONDS) // send pings to keep intermediaries/proxies from dropping idle WS
.retryOnConnectionFailure(true);
if (ConfigurationService.INSTANCE.isDev()) {
HttpUtils.configureInsecureSsl(builder);
}
return builder.build();
}
private static OkHttpClient createLocalHttpClient() {
final var builder = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
// Do not follow redirects automatically; they must be proxied back to the client
.followRedirects(false)
.followSslRedirects(false)
.retryOnConnectionFailure(true);
if (ConfigurationService.INSTANCE.isDev()) {
HttpUtils.configureInsecureSsl(builder);
}
return builder.build();
}
private WebSocket webSocket;
private CountDownLatch closed = new CountDownLatch(1);
private final AtomicBoolean stop = new AtomicBoolean(false);
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(runnable -> {
final var thread = new Thread(runnable, "port-buddy-heartbeat");
thread.setDaemon(true);
return thread;
});
private volatile ScheduledFuture<?> heartbeatTask;
private final ExecutorService requestExecutor = Executors.newFixedThreadPool(4, runnable -> {
final var thread = new Thread(runnable, "port-buddy-http-worker");
thread.setDaemon(true);
return thread;
});
private final Map<String, WebSocket> localWebsocketMap = new ConcurrentHashMap<>();
/**
* Establishes and maintains a blocking WebSocket connection to the server.
* This method constructs a WebSocket connection to a server using a URL
* derived from the server's URL combined with the tunnel identifier. The
* method blocks until the WebSocket connection is closed or interrupted.
* Behavior:
* - Converts the server URL and tunnel identifier into a WebSocket URL.
* - Opens a WebSocket connection to the calculated URL and uses a
* {@code Listener} to handle WebSocket events, such as incoming messages,
* connection closure, or failures.
* - Waits on the {@code closeLatch} to ensure blocking behavior until the
* connection is terminated.
* Exceptions:
* - Catches and handles {@link InterruptedException} if the wait operation
* on the latch is interrupted. Restores the interrupted thread state.
*/
public void runBlocking() {
var backoffMs = 1000L;
final var maxBackoffMs = 30000L;
while (!stop.get()) {
try {
closed = new CountDownLatch(1);
final var wsUrl = toWebSocketUrl(serverUrl, "/api/http-tunnel/" + tunnelId);
final var request = new Request.Builder().url(wsUrl);
if (authToken != null && !authToken.isBlank()) {
request.addHeader("Authorization", "Bearer " + authToken);
}
webSocket = http.newWebSocket(request.build(), new Listener());
// Block until this connection is closed
closed.await();
if (stop.get()) {
break;
}
// Reconnect with backoff
log.info("Tunnel disconnected; reconnecting in {} ms...", backoffMs);
Thread.sleep(backoffMs);
backoffMs = Math.min(backoffMs * 2, maxBackoffMs);
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (final Exception e) {
log.warn("Tunnel loop error: {}", e.toString());
if (verbose) {
e.printStackTrace(System.err);
}
try {
Thread.sleep(backoffMs);
} catch (final InterruptedException ie) {
Thread.currentThread().interrupt();
break;
}
backoffMs = Math.min(backoffMs * 2, maxBackoffMs);
}
}
}
/**
* Closes the WebSocket connection associated with this HTTP tunnel client.
* This method attempts to gracefully close the WebSocket connection, if it exists,
* using the standard WebSocket closure status code 1000 (indicating a normal closure)
* and a reason message "Client exit". If an exception occurs during the closure process,
* it is logged at the debug level and suppressed to ensure that the exception does not
* disrupt the application's flow.
* Behavior:
* - If there is an active WebSocket (represented by the {@code webSocket} field),
* it calls the {@code close} method on the WebSocket instance, passing the closure
* status code and reason message.
* - Logs any exception encountered during the close operation at the debug level
* without re-throwing it.
* Thread-safety: This method is thread-safe, as it uses a local reference to the
* {@code webSocket} field to prevent potential null pointer exceptions caused by
* concurrent modifications.
*/
public void close() {
try {
stop.set(true);
final var task = heartbeatTask;
if (task != null) {
task.cancel(true);
}
scheduler.shutdownNow();
requestExecutor.shutdownNow();
if (webSocket != null) {
webSocket.close(1000, "Client exit");
log.debug("Websocket closed: 1000 OK");
}
localWebsocketMap.values().forEach(ws -> {
try {
ws.close(1000, "Tunnel closed");
} catch (final Exception ignore) {
// ignore
}
});
localWebsocketMap.clear();
} catch (final Exception ignore) {
log.debug("HTTP tunnel close error: {}", ignore.toString());
}
}
private String toWebSocketUrl(final String base, final String path) {
final var uri = URI.create(base);
var scheme = uri.getScheme();
if ("https".equalsIgnoreCase(scheme)) {
scheme = "wss";
} else if ("http".equalsIgnoreCase(scheme)) {
scheme = "ws";
}
final var hostPort = (uri.getPort() == -1) ? uri.getHost() : (uri.getHost() + ":" + uri.getPort());
return scheme + "://" + hostPort + path;
}
private class Listener extends WebSocketListener {
@Override
public void onOpen(final WebSocket webSocket, final Response response) {
log.debug("Tunnel connected to server");
// Start application-level heartbeat PINGs
try {
if (heartbeatTask != null && !heartbeatTask.isCancelled()) {
heartbeatTask.cancel(true);
}
final var config = ConfigurationService.INSTANCE.getConfig();
final var intervalSec = config.getHealthcheckIntervalSec();
heartbeatTask = scheduler.scheduleAtFixedRate(() -> {
try {
final var ping = new ControlMessage();
ping.setType(ControlMessage.Type.PING);
ping.setTs(System.currentTimeMillis());
HttpTunnelClient.this.webSocket.send(MAPPER.writeValueAsString(ping));
} catch (final Exception e) {
log.debug("Heartbeat send failed: {}", e.toString());
}
}, intervalSec, intervalSec, TimeUnit.SECONDS);
} catch (final Exception e) {
log.debug("Failed to start heartbeat: {}", e.toString());
}
}
@Override
public void onMessage(final WebSocket webSocket, final String text) {
try {
log.debug("Received WS message: {}", text);
final var env = MAPPER.readValue(text, MessageEnvelope.class);
if (env.getKind() != null && env.getKind().equals("CTRL")) {
final var ctrl = MAPPER.readValue(text, ControlMessage.class);
if (ctrl.getType() == ControlMessage.Type.EXIT) {
log.info("Received EXIT control message. Shutting down...");
try {
HttpTunnelClient.this.webSocket.close(1000, "Server requested exit");
} catch (final Exception ignore) {
// ignore
}
if (httpLogSink instanceof ConsoleUi ui) {
ui.stop();
}
}
return;
}
if (env.getKind() != null && env.getKind().equals("WS")) {
final var wsMsg = MAPPER.readValue(text, WsTunnelMessage.class);
handleWsFromServer(wsMsg);
return;
}
final var message = MAPPER.readValue(text, HttpTunnelMessage.class);
if (message.getType() == HttpTunnelMessage.Type.REQUEST) {
// Offload request processing to a worker thread to avoid blocking the WS listener
requestExecutor.submit(() -> {
try {
final var resp = handleRequest(message);
final var json = MAPPER.writeValueAsString(resp);
HttpTunnelClient.this.webSocket.send(json);
log.debug("Responded to WS request: {}", resp.getId());
} catch (final Exception ex) {
log.warn("Failed to handle tunneled request {}: {}", message.getId(), ex.toString());
try {
final var error = buildErrorMessage(message.getId(), 502, "Proxy error");
HttpTunnelClient.this.webSocket.send(MAPPER.writeValueAsString(error));
} catch (final Exception e) {
log.error("Failed to send error response: {}", e.getMessage(), e);
}
}
});
} else {
log.debug("Ignoring non-REQUEST msg");
}
} catch (final Exception e) {
log.warn("Failed to process WS message: {}", e.toString());
}
}
@Override
public void onClosed(final WebSocket webSocket, final int code, final String reason) {
log.info("Tunnel closed: {} {}", code, reason);
final var task = heartbeatTask;
if (task != null) {
task.cancel(true);
}
localWebsocketMap.values().forEach(ws -> {
try {
ws.close(1000, "Tunnel closed");
} catch (final Exception ignore) {
// ignore
}
});
localWebsocketMap.clear();
closed.countDown();
}
@Override
public void onFailure(final WebSocket webSocket, final Throwable error, final Response response) {
log.warn("Tunnel failure: {}", error.toString());
final var task = heartbeatTask;
if (task != null) {
task.cancel(true);
}
localWebsocketMap.values().forEach(ws -> {
try {
ws.close(1000, "Tunnel failure");
} catch (final Exception ignore) {
// ignore
}
});
localWebsocketMap.clear();
closed.countDown();
}
}
private void handleWsFromServer(final WsTunnelMessage message) {
final var connId = message.getConnectionId();
switch (message.getWsType()) {
case OPEN -> {
// Connect to local target via WS
final var localWsScheme = "https".equalsIgnoreCase(localScheme) ? "wss" : "ws";
var url = localWsScheme + "://" + localHost + ":" + localPort
+ (message.getPath() != null ? message.getPath() : "/");
if (message.getQuery() != null && !message.getQuery().isBlank()) {
url += "?" + message.getQuery();
}
final var builder = new Request.Builder().url(url);
final var publicHost = URI.create(publicBaseUrl).getHost();
if (publicHost != null) {
builder.header("Host", publicHost);
}
if (message.getHeaders() != null) {
for (final var entry : message.getHeaders().entrySet()) {
if (entry.getKey() != null && entry.getValue() != null) {
if (entry.getKey().equalsIgnoreCase("Host")) {
continue;
}
builder.addHeader(entry.getKey(), entry.getValue());
}
}
}
final var local = http.newWebSocket(builder.build(), new LocalWsListener(connId));
localWebsocketMap.put(connId, local);
}
case TEXT -> {
final var local = localWebsocketMap.get(connId);
if (local != null) {
local.send(message.getText() != null ? message.getText() : "");
}
}
case BINARY -> {
final var local = localWebsocketMap.get(connId);
if (local != null && message.getDataB64() != null) {
local.send(ByteString.of(Base64.getDecoder().decode(message.getDataB64())));
}
}
case CLOSE -> {
final var local = localWebsocketMap.remove(connId);
if (local != null) {
local.close(message.getCloseCode() != null
? message.getCloseCode()
: 1000, message.getCloseReason());
}
}
default -> {
}
}
}
@RequiredArgsConstructor
private class LocalWsListener extends WebSocketListener {
private final String connectionId;
@Override
public void onOpen(final WebSocket webSocket, final Response response) {
try {
final var ack = new WsTunnelMessage();
ack.setWsType(WsTunnelMessage.Type.OPEN_OK);
ack.setConnectionId(connectionId);
HttpTunnelClient.this.webSocket.send(MAPPER.writeValueAsString(ack));
} catch (final Exception ignore) {
log.error("Failed to send local WS open ack: {}", ignore.toString());
}
}
@Override
public void onMessage(final WebSocket webSocket, final String text) {
try {
final var message = new WsTunnelMessage();
message.setWsType(WsTunnelMessage.Type.TEXT);
message.setConnectionId(connectionId);
message.setText(text);
HttpTunnelClient.this.webSocket.send(MAPPER.writeValueAsString(message));
} catch (final Exception e) {
log.debug("Failed to forward local text WS: {}", e.toString());
}
}
@Override
public void onMessage(final WebSocket webSocket, final ByteString bytes) {
try {
final var message = new WsTunnelMessage();
message.setWsType(WsTunnelMessage.Type.BINARY);
message.setConnectionId(connectionId);
message.setDataB64(Base64.getEncoder().encodeToString(bytes.toByteArray()));
HttpTunnelClient.this.webSocket.send(MAPPER.writeValueAsString(message));
} catch (final Exception e) {
log.debug("Failed to forward local binary WS: {}", e.toString());
}
}
@Override
public void onClosed(final WebSocket webSocket, final int code, final String reason) {
try {
localWebsocketMap.remove(connectionId);
final var message = new WsTunnelMessage();
message.setWsType(WsTunnelMessage.Type.CLOSE);
message.setConnectionId(connectionId);
message.setCloseCode(code);
message.setCloseReason(reason);
HttpTunnelClient.this.webSocket.send(MAPPER.writeValueAsString(message));
} catch (final Exception e) {
log.debug("Failed to notify close: {}", e.toString());
}
}
@Override
public void onFailure(final WebSocket webSocket, final Throwable error, final Response response) {
onClosed(webSocket, 1011, error.toString());
}
}
private HttpTunnelMessage handleRequest(final HttpTunnelMessage requestMessage) {
final var method = requestMessage.getMethod();
var url = localScheme + "://" + localHost + ":" + localPort + requestMessage.getPath();
if (requestMessage.getQuery() != null && !requestMessage.getQuery().isBlank()) {
url += "?" + requestMessage.getQuery();
}
final var targetRequest = new Request.Builder()
.url(url)
.method(method, buildBody(method, requestMessage.getBodyB64(), requestMessage.getBodyContentType()));
final var publicHost = URI.create(publicBaseUrl).getHost();
if (publicHost != null) {
targetRequest.header("Host", publicHost);
}
if (requestMessage.getHeaders() != null) {
for (final var header : requestMessage.getHeaders().entrySet()) {
final var name = header.getKey();
final var values = header.getValue();
if (name == null || values == null) {
continue;
}
if (name.equalsIgnoreCase("Host")) {
continue; // Host will be set by client
}
if (name.equalsIgnoreCase("Content-Type")) {
// Content-Type is derived from RequestBody media type
continue;
}
for (final var value : values) {
if (value != null) {
targetRequest.addHeader(name, value);
}
}
}
}
try (final var targetResponse = localHttp.newCall(targetRequest.build()).execute()) {
final var successMessage = new HttpTunnelMessage();
successMessage.setId(requestMessage.getId());
successMessage.setType(HttpTunnelMessage.Type.RESPONSE);
successMessage.setStatus(targetResponse.code());
successMessage.setRespHeaders(extractHeaders(targetResponse));
final var body = targetResponse.body();
if (body != null) {
final var bytes = body.bytes();
if (bytes.length > 0) {
successMessage.setRespBodyB64(Base64.getEncoder().encodeToString(bytes));
}
}
// Log to UI sink
try {
if (httpLogSink != null) {
var displayUrl = publicBaseUrl;
if (requestMessage.getPath() != null) {
displayUrl += requestMessage.getPath();
}
if (requestMessage.getQuery() != null && !requestMessage.getQuery().isBlank()) {
displayUrl += "?" + requestMessage.getQuery();
}
httpLogSink.onHttpLog(method, displayUrl, targetResponse.code());
}
} catch (final Exception ignore) {
log.debug("HTTP log sink failed: {}", ignore.toString());
}
return successMessage;
} catch (final Exception e) {
final var errorMessage = buildErrorMessage(requestMessage.getId(), 502, "Bad Gateway: " + e.getMessage());
try {
if (httpLogSink != null) {
var displayUrl = publicBaseUrl;
if (requestMessage.getPath() != null) {
displayUrl += requestMessage.getPath();
}
if (requestMessage.getQuery() != null && !requestMessage.getQuery().isBlank()) {
displayUrl += "?" + requestMessage.getQuery();
}
httpLogSink.onHttpLog(method, displayUrl, 502);
}
} catch (final Exception ignore) {
log.debug("HTTP log sink failed: {}", ignore.toString());
}
return errorMessage;
}
}
private static HttpTunnelMessage buildErrorMessage(final String id, final int status, final String message) {
final var error = new HttpTunnelMessage();
error.setId(id);
error.setType(HttpTunnelMessage.Type.RESPONSE);
error.setStatus(status);
final var headers = Map.<String, List<String>>of("Content-Type", List.of("text/plain; charset=utf-8"));
error.setRespHeaders(headers);
error.setRespBodyB64(Base64.getEncoder().encodeToString((message).getBytes(StandardCharsets.UTF_8)));
return error;
}
private RequestBody buildBody(final String method, final String bodyB64, final String contentType) {
// Methods that usually don't have body
if (bodyB64 == null) {
return methodSupportsBody(method)
? RequestBody.create(new byte[0], contentType != null ? MediaType.parse(contentType) : null)
: null;
}
final var bytes = Base64.getDecoder().decode(bodyB64);
final var mediaType = contentType != null && !contentType.isBlank()
? MediaType.parse(contentType)
: MediaType.parse("application/octet-stream");
return RequestBody.create(bytes, mediaType);
}
private boolean methodSupportsBody(final String method) {
if (method == null) {
return false;
}
return switch (method.toUpperCase()) {
case "POST", "PUT", "PATCH" -> true;
default -> false;
};
}
private Map<String, List<String>> extractHeaders(final Response response) {
final var map = new HashMap<String, List<String>>();
for (final var name : response.headers().names()) {
final var values = response.headers(name);
if (values != null && !values.isEmpty()) {
map.put(name, new ArrayList<>(values));
}
}
return map;
}
}
================================================
FILE: cli/src/main/java/tech/amak/portbuddy/cli/tunnel/NetTunnelClient.java
================================================
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tech.amak.portbuddy.cli.tunnel;
import static tech.amak.portbuddy.cli.utils.JsonUtils.MAPPER;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import okio.ByteString;
import tech.amak.portbuddy.cli.config.ConfigurationService;
import tech.amak.portbuddy.cli.ui.ConsoleUi;
import tech.amak.portbuddy.cli.ui.NetTrafficSink;
import tech.amak.portbuddy.cli.utils.HttpUtils;
import tech.amak.portbuddy.common.TunnelType;
import tech.amak.portbuddy.common.tunnel.BinaryWsFrame;
import tech.amak.portbuddy.common.tunnel.ControlMessage;
import tech.amak.portbuddy.common.tunnel.MessageEnvelope;
import tech.amak.portbuddy.common.tunnel.WsTunnelMessage;
@Slf4j
@RequiredArgsConstructor
public class NetTunnelClient {
private final String proxyHost;
private final int proxyHttpPort;
/**
* Whether the WebSocket should use TLS (wss). Must reflect the scheme of the configured server URL.
*/
private final boolean secure;
private final UUID tunnelId;
private final String localHost;
private final int localPort;
private final TunnelType tunnelType;
// Expected public connection details returned by the server during expose REST call
private final String expectedPublicHost;
private final int expectedPublicPort;
private final String authToken; // Bearer token if available
private final NetTrafficSink trafficSink;
private final boolean verbose;
private final OkHttpClient http = HttpUtils.createClient();
private final OkHttpClient rest = HttpUtils.createClient();
private WebSocket webSocket;
private final Map<String, LocalTcp> locals = new ConcurrentHashMap<>();
private final Map<String, LocalUdp> udpLocals = new ConcurrentHashMap<>();
private CountDownLatch closed = new CountDownLatch(1);
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(runnable -> {
final var thread = new Thread(runnable, "pb-net-heartbeat");
thread.setDaemon(true);
return thread;
});
private volatile ScheduledFuture<?> heartbeatTask;
private volatile ScheduledFuture<?> wsHeartbeatTask;
private final AtomicBoolean closedReported = new AtomicBoolean(false);
private final AtomicBoolean stop = new AtomicBoolean(false);
private final AtomicBoolean warnedAboutReassignment = new AtomicBoolean(false);
private final AtomicBoolean successfullyConnected = new AtomicBoolean(false);
/**
* Establishes and maintains a WebSocket connection for TCP/UDP tunneling.
* This method constructs the WebSocket URL using the configured proxy host, port, and tunnel ID.
* It sets up an authentication token in the request header, if provided, and initializes the WebSocket connection.
* The method blocks the current thread until the connection is closed or an interruption occurs.
* Behavior:
* - Converts a base HTTP URL to a WebSocket URL using the {@code toWebSocketUrl} method.
* - Adds an optional "Authorization" header to the WebSocket request for authentication.
* - Creates a WebSocket connection using the provided URL and the {@code Listener} for handling events.
* - Waits for the WebSocket connection to close by utilizing a {@code CountDownLatch}.
* - Handles interruptions by setting the thread's interrupt status.
*/
public void runBlocking() {
var backoffMs = 1000L;
final var maxBackoffMs = 30000L;
while (!stop.get()) {
try {
closed = new CountDownLatch(1);
final var scheme = secure ? "https://" : "http://";
final var publicHostParam = (expectedPublicHost == null || expectedPublicHost.isBlank())
? ""
: "&public-host=" + URLEncoder.encode(expectedPublicHost, StandardCharsets.UTF_8);
final var path = "/api/net-tunnel/" + tunnelId
+ "?type=" + tunnelType.name().toLowerCase()
+ "&port=" + expectedPublicPort
+ publicHostParam;
final var url = toWebSocketUrl(scheme + proxyHost + ":" + proxyHttpPort, path);
final var request = new Request.Builder().url(url);
if (authToken != null && !authToken.isBlank()) {
request.addHeader("Authorization", "Bearer " + authToken);
}
webSocket = http.newWebSocket(request.build(), new Listener());
successfullyConnected.set(false);
// Block until this connection is closed
closed.await();
if (stop.get()) {
break;
}
if (successfullyConnected.get()) {
backoffMs = 1000L;
}
// Reconnect with backoff
log.info("Net tunnel disconnected; reconnecting in {} ms...", backoffMs);
Thread.sleep(backoffMs);
backoffMs = Math.min(backoffMs * 2, maxBackoffMs);
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (final Exception e) {
log.warn("Net tunnel loop error: {}", e.toString());
if (verbose) {
e.printStackTrace(System.err);
}
try {
Thread.sleep(backoffMs);
} catch (final InterruptedException ie) {
Thread.currentThread().interrupt();
break;
}
backoffMs = Math.min(backoffMs * 2, maxBackoffMs);
}
}
}
/**
* Closes the WebSocket connection for the TCP tunnel client.
* This method attempts to gracefully close the WebSocket connection, if it exists,
* by sending a close frame with a status code of 1000 (normal closure) and a reason
* message ("Client exit"). If an exception occurs during the close operation, the
* error is logged for debugging purposes.
* Behavior:
* - Checks if the WebSocket instance (`ws`) is not null.
* - If the WebSocket exists, sends a close frame with a normal status and reason.
* - Catches and logs any exceptions encountered during the close operation,
* ensuring the process does not disrupt the program flow.
*/
public void close() {
try {
stop.set(true);
final var task = heartbeatTask;
if (task != null) {
task.cancel(true);
}
final var wsTask = wsHeartbeatTask;
if (wsTask != null) {
wsTask.cancel(true);
}
scheduler.shutdownNow();
if (webSocket != null) {
webSocket.close(1000, "Client exit");
}
locals.values().forEach(this::close);
locals.clear();
udpLocals.values().forEach(this::close);
udpLocals.clear();
reportClosedSafe();
} catch (final Exception e) {
log.debug("TCP tunnel close error: {}", e.toString());
}
}
private void close(final LocalTcp localTcp) {
if (localTcp != null) {
try {
localTcp.sock.close();
} catch (final Exception e) {
log.debug("Failed to close TCP: {}", e.toString());
}
}
}
private void close(final LocalUdp localUdp) {
if (localUdp != null) {
try {
localUdp.sock.close();
} catch (final Exception e) {
log.debug("Failed to close UDP local: {}", e.toString());
}
}
}
private String toWebSocketUrl(final String httpUri, final String path) {
var uri = httpUri;
if (uri.startsWith("http://")) {
uri = "ws://" + uri.substring(7);
} else if (uri.startsWith("https://")) {
uri = "wss://" + uri.substring(8);
}
if (uri.endsWith("/")) {
uri = uri.substring(0, uri.length() - 1);
}
return uri + path;
}
private class Listener extends WebSocketListener {
@Override
public void onOpen(final WebSocket webSocket, final Response response) {
successfullyConnected.set(true);
// Report CONNECTED and start heartbeats
try {
postStatus("/api/tunnels/" + tunnelId + "/connected");
} catch (final Exception e) {
log.debug("Failed to report NET connected: {}", e.toString());
}
// allow reporting CLOSED again for future disconnects after a successful reconnect
closedReported.set(false);
final var config = ConfigurationService.INSTANCE.getConfig();
final var intervalSec = Math.max(1, config.getHealthcheckIntervalSec());
try {
final var existing = heartbeatTask;
if (existing != null && !existing.isCancelled()) {
existing.cancel(true);
}
heartbeatTask = scheduler.scheduleAtFixedRate(() -> {
try {
postStatus("/api/tunnels/" + tunnelId + "/heartbeat");
} catch (final Exception e) {
log.debug("NET heartbeat failed: {}", e.toString());
}
}, 1, intervalSec, TimeUnit.SECONDS);
} catch (final Exception e) {
log.debug("Failed to start NET heartbeat: {}", e.toString());
}
// Start WS application-level heartbeat (PING/PONG)
try {
final var existingWs = wsHeartbeatTask;
if (existingWs != null && !existingWs.isCancelled()) {
existingWs.cancel(true);
}
wsHeartbeatTask = scheduler.scheduleAtFixedRate(() -> {
try {
final var ping = new ControlMessage();
ping.setType(ControlMessage.Type.PING);
ping.setTs(System.currentTimeMillis());
NetTunnelClient.this.webSocket.send(MAPPER.writeValueAsString(ping));
} catch (final Exception e) {
log.debug("WS heartbeat send failed: {}", e.toString());
}
}, intervalSec, intervalSec, TimeUnit.SECONDS);
} catch (final Exception e) {
log.debug("Failed to start WS heartbeat: {}", e.toString());
}
}
@Override
public void onMessage(final WebSocket webSocket, final String text) {
try {
final var env = MAPPER.readValue(text, MessageEnvelope.class);
if (env.getKind() != null && env.getKind().equals("CTRL")) {
final var ctrl = MAPPER.readValue(text, ControlMessage.class);
if (ctrl.getType() == ControlMessage.Type.EXIT) {
log.info("Received EXIT control message. Shutting down...");
try {
NetTunnelClient.this.webSocket.close(1000, "Server requested exit");
} catch (final Exception ignore) {
// ignore
}
if (trafficSink instanceof ConsoleUi ui) {
ui.stop();
}
}
return;
}
if (env.getKind() != null && env.getKind().equals("WS")) {
final var msg = MAPPER.readValue(text, WsTunnelMessage.class);
handleControl(msg);
}
// Unknown kinds are ignored for NET tunnels
} catch (final Exception e) {
log.warn("Failed to process WS text message: {}", e.toString());
}
}
@Override
public void onMessage(final WebSocket webSocket, final ByteString bytes) {
try {
final var decoded = BinaryWsFrame.decode(bytes.toByteArray());
if (decoded == null) {
return;
}
if (tunnelType == TunnelType.TCP) {
final var local = locals.get(decoded.connectionId());
if (local != null) {
try {
local.out.write(decoded.data());
local.out.flush();
if (trafficSink != null) {
trafficSink.onBytesIn(decoded.data().length);
}
} catch (final Exception e) {
log.debug("Write to local TCP failed: {}", e.toString());
}
}
} else if (tunnelType == TunnelType.UDP) {
// For UDP, forward the datagram to local UDP server using per-connection socket
final var connId = decoded.connectionId();
var localUdp = udpLocals.get(connId);
if (localUdp == null) {
try {
final var sock = new DatagramSocket();
localUdp = new LocalUdp(connId, sock);
udpLocals.put(connId, localUdp);
// start receive loop for this connection
final var localUdpRef = localUdp;
new Thread(() -> pumpUdpLocalToProxy(localUdpRef)).start();
} catch (final Exception e) {
log.debug("Failed to create local UDP socket: {}", e.toString());
return;
}
}
try {
final var packet = new DatagramPacket(decoded.data(), decoded.data().length,
new InetSocketAddress(localHost, localPort));
localUdp.sock.send(packet);
if (trafficSink != null) {
trafficSink.onBytesIn(decoded.data().length);
}
} catch (final Exception e) {
log.debug("Write to local UDP failed: {}", e.toString());
}
}
} catch (final Exception e) {
log.debug("Failed to handle binary WS frame: {}", e.toString());
}
}
@Override
public void onClosed(final WebSocket webSocket, final int code, final String reason) {
log.info("Tunnel closed: {} {}", code, reason);
final var task = heartbeatTask;
if (task != null) {
task.cancel(true);
}
final var wsTask = wsHeartbeatTask;
if (wsTask != null) {
wsTask.cancel(true);
}
locals.values().forEach(NetTunnelClient.this::close);
locals.clear();
udpLocals.values().forEach(NetTunnelClient.this::close);
udpLocals.clear();
reportClosedSafe();
closed.countDown();
}
@Override
public void onFailure(final WebSocket webSocket, final Throwable throwable, final Response response) {
log.warn("Tunnel failure: {}", throwable.toString());
final var task = heartbeatTask;
if (task != null) {
task.cancel(true);
}
final var wsTask = wsHeartbeatTask;
if (wsTask != null) {
wsTask.cancel(true);
}
locals.values().forEach(NetTunnelClient.this::close);
locals.clear();
udpLocals.values().forEach(NetTunnelClient.this::close);
udpLocals.clear();
reportClosedSafe();
closed.countDown();
}
}
private void reportClosedSafe() {
if (closedReported.compareAndSet(false, true)) {
try {
postStatus("/api/tunnels/" + tunnelId + "/closed");
} catch (final Exception e) {
log.debug("Failed to report NET closed: {}", e.toString());
}
}
}
private void handleControl(final WsTunnelMessage message) throws Exception {
final var connId = message.getConnectionId();
switch (message.getWsType()) {
case EXPOSED -> {
final var actualHost = message.getPublicHost();
final var actualPort = message.getPublicPort();
if (actualHost != null && actualPort != null) {
final var hostDiffers = expectedPublicHost != null && !expectedPublicHost.equals(actualHost);
final var portDiffers = expectedPublicPort != actualPort;
if ((hostDiffers || portDiffers) && warnedAboutReassignment.compareAndSet(false, true)) {
System.out.printf(
"Warning: requested public %s:%d but exposed on %s:%d%n",
expectedPublicHost,
expectedPublicPort,
actualHost,
actualPort
);
}
}
}
case OPEN -> {
if (tunnelType == TunnelType.TCP) {
// Establish local TCP
final var socket = new Socket();
socket.connect(new InetSocketAddress(localHost, localPort), 5000);
final var local = new LocalTcp(connId, socket);
locals.put(connId, local);
// Ack
final var ack = new WsTunnelMessage();
ack.setWsType(WsTunnelMessage.Type.OPEN_OK);
ack.setConnectionId(connId);
webSocket.send(MAPPER.writeValueAsString(ack));
// Start reader thread from local TCP to proxy WS
new Thread(() -> pumpLocalToProxy(local)).start();
} else {
// UDP does not use OPEN for per-flow; ignore or acknowledge for compatibility
final var ack = new WsTunnelMessage();
ack.setWsType(WsTunnelMessage.Type.OPEN_OK);
ack.setConnectionId(connId);
webSocket.send(MAPPER.writeValueAsString(ack));
}
}
case BINARY -> {
if (tunnelType == TunnelType.TCP) {
// Base64 payload from proxy to local TCP (legacy)
final var local = locals.get(connId);
if (local != null && message.getDataB64() != null) {
try {
final var bytes = Base64.getDecoder().decode(message.getDataB64());
local.out.write(bytes);
local.out.flush();
if (trafficSink != null) {
trafficSink.onBytesIn(bytes.length);
}
} catch (final Exception e) {
log.debug("Write to local TCP failed: {}", e.toString());
}
}
} else if (tunnelType == TunnelType.UDP) {
// Legacy TEXT BINARY for UDP: forward to local as datagram
if (message.getDataB64() != null) {
var localUdp = udpLocals.get(connId);
if (localUdp == null) {
final var sock = new DatagramSocket();
localUdp = new LocalUdp(connId, sock);
udpLocals.put(connId, localUdp);
final var localUdpRef = localUdp;
new Thread(() -> pumpUdpLocalToProxy(localUdpRef)).start();
}
final var bytes = Base64.getDecoder().decode(message.getDataB64());
final var packet = new DatagramPacket(bytes, bytes.length,
new InetSocketAddress(localHost, localPort));
localUdp.sock.send(packet);
if (trafficSink != null) {
trafficSink.onBytesIn(bytes.length);
}
}
}
}
case CLOSE -> {
if (tunnelType == TunnelType.TCP) {
close(locals.remove(connId));
} else {
close(udpLocals.remove(connId));
}
}
default -> {
}
}
}
private void pumpLocalToProxy(final LocalTcp local) {
final var buffer = new byte[8192];
try {
while (true) {
final var byteCount = local.in.read(buffer);
if (byteCount == -1) {
break;
}
final var frame = BinaryWsFrame.encodeToArray(local.connectionId, buffer, 0, byteCount);
final var byteString = ByteString.of(frame);
webSocket.send(byteString);
if (trafficSink != null) {
trafficSink.onBytesOut(byteCount);
}
}
} catch (final Exception e) {
// ignore
} finally {
try {
final var message = new WsTunnelMessage();
message.setWsType(WsTunnelMessage.Type.CLOSE);
message.setConnectionId(local.connectionId);
webSocket.send(MAPPER.writeValueAsString(message));
} catch (final Exception e) {
log.error("Failed to send local WS close: {}", e.toString());
}
close(local);
locals.remove(local.connectionId);
}
}
private static class LocalTcp {
final String connectionId;
final Socket sock;
final InputStream in;
final OutputStream out;
LocalTcp(final String connectionId, final Socket sock) throws Exception {
this.connectionId = connectionId;
this.sock = sock;
this.in = sock.getInputStream();
this.out = sock.getOutputStream();
}
}
private static class LocalUdp {
final String connectionId;
final DatagramSocket sock;
LocalUdp(final String connectionId, final DatagramSocket sock) {
this.connectionId = connectionId;
this.sock = sock;
}
}
private void pumpUdpLocalToProxy(final LocalUdp local) {
final var buffer = new byte[65535];
try {
while (!local.sock.isClosed()) {
final var packet = new DatagramPacket(buffer, buffer.length);
local.sock.receive(packet);
final var frame = BinaryWsFrame
.encodeToArray(local.connectionId, packet.getData(), packet.getOffset(), packet.getLength());
final var byteString = ByteString.of(frame);
webSocket.send(byteString);
if (trafficSink != null) {
trafficSink.onBytesOut(packet.getLength());
}
}
} catch (final Exception e) {
// ignore normal close
} finally {
close(local);
udpLocals.remove(local.connectionId);
}
}
private void postStatus(final String path) throws Exception {
final var base = (secure ? "https://" : "http://") + proxyHost + ":" + proxyHttpPort;
final var url = base + path;
final var body = RequestBody.create("{}", MediaType.parse("application/json"));
final var builder = new Request.Builder().url(url).post(body);
if (authToken != null && !authToken.isBlank()) {
builder.header("Authorization", "Bearer " + authToken);
}
try (final var response = rest.newCall(builder.build()).execute()) {
if (!response.isSuccessful()) {
log.debug("Status POST failed {} {} for {}", response.code(), response.message(), path);
}
}
}
}
================================================
FILE: cli/src/main/java/tech/amak/portbuddy/cli/ui/ConsoleUi.java
================================================
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tech.amak.portbuddy.cli.ui;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.Duration;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import org.jline.reader.LineReaderBuilder;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
import org.jline.utils.InfoCmp;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import tech.amak.portbuddy.cli.config.ConfigurationService;
import tech.amak.portbuddy.common.ClientConfig;
import tech.amak.portbuddy.common.TunnelType;
import tech.amak.portbuddy.common.dto.auth.RegisterRequest;
@Slf4j
@RequiredArgsConstructor
public class ConsoleUi implements HttpLogSink, NetTrafficSink {
public record HttpLog(String method, String url, int status) {
}
private final TunnelType tunnelType;
private final String localDetails;
private final String publicDetails;
private Terminal terminal;
private PrintWriter out;
private final Deque<HttpLog> httpLogs = new ArrayDeque<>();
private final AtomicLong inBytes = new AtomicLong();
private final AtomicLong outBytes = new AtomicLong();
private final AtomicBoolean running = new AtomicBoolean(false);
private final CountDownLatch exit = new CountDownLatch(1);
private final ClientConfig config = ConfigurationService.INSTANCE.getConfig();
private Thread renderThread;
@Getter
@Setter
private Runnable onExit;
/**
* Prompts the user for registration details using the console.
* When no API key is initialized, the user should only be asked for the email address.
*
* @return a RegisterRequest containing the input data (email only)
* @throws IOException if an I/O error occurs or console is not available
*/
public static RegisterRequest promptForUserRegistration() throws IOException {
try (final var terminal = buildTerminal()) {
final var reader = LineReaderBuilder.builder().terminal(terminal).build();
String email;
while (true) {
email = reader.readLine("Email: ");
if (isValidEmail(email)) {
break;
}
terminal.writer().println("Invalid email address. Please try again.");
terminal.flush();
}
// Only email is required for registration. Name and password are set on the server side.
return new RegisterRequest(email, null, null);
}
}
private static boolean isValidEmail(final String email) {
return email != null && email.matches("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$");
}
private static Terminal buildTerminal() throws IOException {
final var terminal = TerminalBuilder.builder()
.streams(System.in, System.out)
.system(true)
.jansi(true)
.jna(true)
.jni(true)
.ffm(true)
.exec(true)
.dumb(true)
.build();
return terminal;
}
/**
* Starts the Console UI. This method initializes the terminal for interactive
* output, sets up signal handling for interrupt signals, and spawns a new thread
* to handle the rendering loop.
* The method ensures that the UI is started only once by using an atomic flag. If
* the UI is already running, the method exits early.
* In case of initialization failure, such as terminal setup issues, an
* IllegalStateException is thrown to indicate that the UI failed to start.
* Once started, the rendering process runs on a daemon thread that periodically
* updates the terminal output. The rendering loop works until instructed to stop
* by external signals or method calls.
*/
public void start() {
if (running.getAndSet(true)) {
return;
}
try {
terminal = buildTerminal();
out = terminal.writer();
terminal.handle(Terminal.Signal.INT, signal -> stop());
} catch (final Exception e) {
throw new IllegalStateException("Failed to start console UI", e);
}
clear();
out.printf("Port Buddy - Mode: %s%n", tunnelType.name().toLowerCase());
out.println();
out.printf("Local: %s%n", localDetails);
out.printf("Public: %s%n", publicDetails);
out.println();
out.println("Press Ctrl+C to exit");
out.flush();
if (config.isLogEnabled()) {
out.println("----------------------------------------------");
out.println();
if (tunnelType == TunnelType.HTTP) {
out.println("HTTP requests log:");
} else {
out.println(tunnelType + " traffic:");
}
out.flush();
renderThread = new Thread(this::renderLoop, "port-buddy-ui");
renderThread.setDaemon(true);
renderThread.start();
}
}
/**
* Waits for the termination signal of the Console UI. This method blocks the
* current thread until the internal exit condition is triggered, allowing
* proper synchronization for dependent operations.
* In case the thread is interrupted while waiting, the interrupt status is
* restored to ensure the interruption can be detected by other parts of the
* application.
*/
public void waitForExit() {
try {
exit.await();
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
}
}
/**
* Stops the Console UI. This method transitions the system out of a running state,
* allowing for a clean shutdown. It ensures that the rendering loop halts,
* decrements the exit latch, and executes the optional exit callback if provided.
* The method is thread-safe and idempotent; it ensures that stopping the UI
* multiple times has no adverse effects. If the UI is not currently running,
* the method exits without performing any operations.
* If an {@link Runnable} callback is set via {@link #setOnExit(Runnable)},
* the callback is executed upon stopping. Any exceptions thrown by the callback
* are caught and logged to avoid disrupting the shutdown process.
*/
public void stop() {
if (!running.getAndSet(false)) {
return;
}
exit.countDown();
if (onExit != null) {
try {
onExit.run();
} catch (final Exception e) {
log.warn("onExit handler failed: {}", e.toString());
}
}
}
@Override
public void onHttpLog(final String method, final String url, final int status) {
synchronized (httpLogs) {
if (httpLogs.size() == config.getLogLinesCount()) {
httpLogs.removeFirst();
}
httpLogs.addLast(new HttpLog(method, url, status));
}
}
@Override
public void onBytesIn(final long bytes) {
inBytes.addAndGet(Math.max(0, bytes));
}
@Override
public void onBytesOut(final long bytes) {
outBytes.addAndGet(Math.max(0, bytes));
}
private void renderLoop() {
final var frameDelay = Duration.ofMillis(config.getConsoleFrameDelayMs());
while (running.get()) {
try {
terminal.puts(InfoCmp.Capability.cursor_address, 9, 0);
terminal.flush();
render();
Thread.sleep(frameDelay.toMillis());
} catch (final Exception e) {
log.debug("Render loop error: {}", e.toString());
}
}
clear();
}
private void clear() {
try {
terminal.puts(InfoCmp.Capability.clear_screen);
terminal.flush();
} catch (final Exception ignore) {
// ignore
}
}
private void render() {
if (tunnelType == TunnelType.HTTP) {
synchronized (httpLogs) {
if (httpLogs.isEmpty()) {
out.println("(no requests yet)");
} else {
httpLogs.forEach(httpLog -> {
terminal.puts(InfoCmp.Capability.clr_eol);
terminal.flush();
out.printf("%-6s %-3d %s%n", safe(httpLog.method()), httpLog.status(), safe(httpLog.url()));
});
}
}
} else {
final var inKb = inBytes.get() / 1024.0;
final var outKb = outBytes.get() / 1024.0;
out.printf("IN %.2f KB | OUT %.2f KB%n", inKb, outKb);
}
out.flush();
}
private String safe(final String value) {
return value == null ? "" : value;
}
}
================================================
FILE: cli/src/main/java/tech/amak/portbuddy/cli/ui/HttpLogSink.java
================================================
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tech.amak.portbuddy.cli.ui;
public interface HttpLogSink {
void onHttpLog(final String method, final String url, final int status);
}
================================================
FILE: cli/src/main/java/tech/amak/portbuddy/cli/ui/NetTrafficSink.java
================================================
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tech.amak.portbuddy.cli.ui;
public interface NetTrafficSink {
void onBytesIn(final long bytes);
void onBytesOut(final long bytes);
}
================================================
FILE: cli/src/main/java/tech/amak/portbuddy/cli/utils/HttpUtils.java
================================================
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tech.amak.portbuddy.cli.utils;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import tech.amak.portbuddy.cli.config.ConfigurationService;
@Slf4j
@UtilityClass
public class HttpUtils {
/**
* Creates a new OkHttpClient with default configuration.
* If the application is running in dev mode, it disables SSL verification.
*
* @return a configured OkHttpClient instance
*/
public static OkHttpClient createClient() {
final var builder = new OkHttpClient.Builder();
if (ConfigurationService.INSTANCE.isDev()) {
configureInsecureSsl(builder);
}
return builder.build();
}
/**
* Configures the provided OkHttpClient.Builder to trust all SSL certificates.
* Use with caution, primarily intended for development environments with self-signed certificates.
*
* @param builder the OkHttpClient.Builder to configure
*/
public static void configureInsecureSsl(final OkHttpClient.Builder builder) {
try {
final var trustAllCerts = new TrustManager[] {
new X509TrustManager() {
@Override
public void checkClientTrusted(final X509Certificate[] chain, final String authType)
throws CertificateException {
}
@Override
public void checkServerTrusted(final X509Certificate[] chain, final String authType)
throws CertificateException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[] {};
}
}
};
final var sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
final var sslSocketFactory = sslContext.getSocketFactory();
builder.sslSocketFactory(sslSocketFactory, (X509TrustManager) trustAllCerts[0]);
builder.hostnameVerifier((hostname, session) -> true);
} catch (final Exception e) {
log.error("Failed to configure insecure SSL: {}", e.toString());
}
}
}
================================================
FILE: cli/src/main/java/tech/amak/portbuddy/cli/utils/JsonUtils.java
================================================
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tech.amak.portbuddy.cli.utils;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class JsonUtils {
public static final ObjectMapper MAPPER = new ObjectMapper();
static {
MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
}
================================================
FILE: cli/src/main/resources/META-INF/native-image/tech.amak/port-buddy-cli/reflect-config.json
================================================
[
{
"name": "tech.amak.portbuddy.common.dto.auth.RegisterRequest",
"allDeclaredConstructors": true,
"allPublicMethods": true,
"allDeclaredFields": true
},
{
"name": "tech.amak.portbuddy.common.dto.auth.RegisterResponse",
"allDeclaredConstructors": true,
"allPublicMethods": true,
"allDeclaredFields": true
},
{
"name": "tech.amak.portbuddy.common.dto.auth.TokenExchangeRequest",
"allDeclaredConstructors": true,
"allPublicMethods": true,
"allDeclaredFields": true
},
{
"name": "tech.amak.portbuddy.common.dto.auth.TokenExchangeResponse",
"allDeclaredConstructors": true,
"allPublicMethods": true,
"allDeclaredFields": true
},
{
"name": "tech.amak.portbuddy.common.dto.ExposeRequest",
"allDeclaredConstructors": true,
"allPublicMethods": true,
"allDeclaredFields": true,
"queryAllDeclaredMethods": true
},
{
"name": "tech.amak.portbuddy.common.dto.ExposeResponse",
"allDeclaredConstructors": true,
"allPublicMethods": true,
"allDeclaredFields": true,
"queryAllDeclaredMethods": true
},
{
"name": "tech.amak.portbuddy.common.ClientConfig",
"allDeclaredConstructors": true,
"allPublicMethods": true,
"allDeclaredFields": true
},
{
"name": "tech.amak.portbuddy.common.TunnelType",
"allDeclaredConstructors": true,
"allPublicMethods": true,
"allDeclaredFields": true
},
{
"name": "java.util.UUID",
"allDeclaredConstructors": true,
"allPublicMethods": true
},
{
"name": "tech.amak.portbuddy.cli.PortBuddy$HostPort",
"allDeclaredConstructors": true,
"allPublicMethods": true,
"allDeclaredFields": true
},
{
"name": "tech.amak.portbuddy.common.tunnel.ControlMessage",
"allDeclaredConstructors": true,
"allPublicMethods": true,
"allDeclaredFields": true
},
{
"name": "tech.amak.portbuddy.common.tunnel.ControlMessage$Type",
"allPublicMethods": true
},
{
"name": "tech.amak.portbuddy.common.tunnel.HttpTunnelMessage",
"allDeclaredConstructors": true,
"allPublicMethods": true,
"allDeclaredFields": true
},
{
"name": "tech.amak.portbuddy.common.tunnel.HttpTunnelMessage$Type",
"allPublicMethods": true
},
{
"name": "tech.amak.portbuddy.common.tunnel.MessageEnvelope",
"allDeclaredConstructors": true,
"allPublicMethods": true,
"allDeclaredFields": true
},
{
"name": "tech.amak.portbuddy.common.tunnel.WsTunnelMessage",
"allDeclaredConstructors": true,
"allPublicMethods": true,
"allDeclaredFields": true
},
{
"name": "tech.amak.portbuddy.common.tunnel.WsTunnelMessage$Type",
"allPublicMethods": true
},
{
"name": "tech.amak.portbuddy.common.tunnel.BinaryWsFrame$Decoded",
"allDeclaredConstructors": true,
"allPublicMethods": true,
"allDeclaredFields": true
},
{
"name": "com.fasterxml.jackson.dataformat.yaml.YAMLFactory",
"allDeclaredConstructors": true
},
{
"name": "com.fasterxml.jackson.dataformat.yaml.YAMLGenerator$Feature",
"allPublicMethods": true
},
{
"name": "com.fasterxml.jackson.dataformat.yaml.YAMLParser$Feature",
"allPublicMethods": true
}
]
================================================
FILE: cli/src/main/resources/META-INF/native-image/tech.amak/port-buddy-cli/resource-config.json
================================================
{
"resources": [
{
"pattern": "application\\.yml"
},
{
"pattern": "application-dev\\.yml"
},
{
"pattern": "META-INF/services/ch.qos.logback.classic.spi.Configurator"
},
{
"pattern": "logback\\.xml"
},
{
"pattern": "logback-test\\.xml"
}
],
"bundles": []
}
================================================
FILE: cli/src/main/resources/application-dev.yml
================================================
#serverUrl: "https://localhost:8443"
serverUrl: https://localhost:8443
logEnabled: false
================================================
FILE: cli/src/main/resources/application.yml
================================================
serverUrl: "https://portbuddy.dev"
logEnabled: true
================================================
FILE: cli/src/main/resources/logback.xml
================================================
<!--
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
~
-->
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="error">
<appender-ref ref="STDOUT" />
</root>
</configuration>
================================================
FILE: common/pom.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>tech.amak</groupId>
<artifactId>port-buddy</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>common</artifactId>
<name>port-buddy-common</name>
<properties>
<maven.compiler.release>25</maven.compiler.release>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.18.1</version>
</dependency>
</dependencies>
</project>
================================================
FILE: common/src/main/java/tech/amak/portbuddy/common/ClientConfig.java
================================================
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tech.amak.portbuddy.common;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ClientConfig {
@JsonProperty("apiToken")
private String apiToken;
@JsonProperty("serverUrl")
private String serverUrl;
@JsonProperty("logLinesCount")
private int logLinesCount = 20;
@JsonProperty("logEnabled")
private boolean logEnabled = false;
@JsonProperty("consoleFrameDelay")
private int consoleFrameDelayMs = 200;
@JsonProperty("healthcheckIntervalSec")
private int healthcheckIntervalSec = 5;
}
================================================
FILE: common/src/main/java/tech/amak/portbuddy/common/Plan.java
================================================
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tech.amak.portbuddy.common;
/** Subscription plans. */
public enum Plan {
PRO,
TEAM
}
================================================
FILE: common/src/main/java/tech/amak/portbuddy/common/TunnelType.java
================================================
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tech.amak.portbuddy.common;
/**
* Supported expose modes.
*/
public enum TunnelType {
HTTP,
TCP,
UDP;
/**
* Converts a string representation of a mode to its corresponding {@code Mode} enum value.
* If the provided string is {@code null}, defaults to {@code HTTP}.
*
* @param mode the string representation of the mode, such as "http" or "tcp".
* Case-insensitive. If {@code null}, the method returns {@code HTTP}.
* @return the corresponding {@code Mode} enum value.
* @throws IllegalArgumentException if the string does not match any supported mode.
*/
public static TunnelType from(final String mode) {
if (mode == null) {
return HTTP;
}
return switch (mode.toLowerCase()) {
case "http" -> HTTP;
case "tcp" -> TCP;
case "udp" -> UDP;
default -> throw new IllegalArgumentException("Unknown mode: " + mode);
};
}
}
================================================
FILE: common/src/main/java/tech/amak/portbuddy/common/dto/DnsInstructionsEmailRequest.java
================================================
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tech.amak.portbuddy.common.dto;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import lombok.Builder;
import lombok.Data;
import lombok.extern.jackson.Jacksonized;
@Data
@Builder
@Jacksonized
public class DnsInstructionsEmailRequest {
private final UUID jobId;
private final String domain;
private final String contactEmail;
private final List<Map<String, String>> records;
private final OffsetDateTime expiresAt;
}
================================================
FILE: common/src/main/java/tech/amak/portbuddy/common/dto/ExposeRequest.java
================================================
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tech.amak.portbuddy.common.dto;
import tech.amak.portbuddy.common.TunnelType;
/** Request to expose a local HTTP/TCP/UDP service. */
public record ExposeRequest(
TunnelType tunnelType,
String scheme,
String host,
int port,
String domain,
String portReservation /* optional, format: host:port for TCP/UDP */,
String passcode /* optional, for HTTP tunnels only */
) {}
================================================
FILE: common/src/main/java/tech/amak/portbuddy/common/dto/ExposeResponse.java
================================================
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tech.amak.portbuddy.common.dto;
import java.util.UUID;
import com.fasterxml.jackson.annotation.JsonInclude;
/**
* Response with public exposure details.
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public record ExposeResponse(
String source,
String publicUrl,
String publicHost,
Integer publicPort,
UUID tunnelId,
String subdomain
) {
}
================================================
FILE: common/src/main/java/tech/amak/portbuddy/common/dto/auth/RegisterRequest.java
================================================
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tech.amak.portbuddy.common.dto.auth;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class RegisterRequest {
@JsonProperty("email")
private String email;
@JsonProperty("name")
private String name;
@JsonProperty("password")
private String password;
}
================================================
FILE: common/src/main/java/tech/amak/portbuddy/common/dto/auth/RegisterResponse.java
================================================
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tech.amak.portbuddy.common.dto.auth;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class RegisterResponse {
@JsonProperty("apiKey")
private String apiKey;
@JsonProperty("success")
private boolean success;
@JsonProperty("message")
private String message;
@JsonProperty("statusCode")
private int statusCode;
}
================================================
FILE: common/src/main/java/tech/amak/portbuddy/common/dto/auth/TokenExchangeRequest.java
================================================
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tech.amak.portbuddy.common.dto.auth;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class TokenExchangeRequest {
@JsonProperty("apiToken")
private String apiToken;
@JsonProperty("cliClientVersion")
private String cliClientVersion;
}
================================================
FILE: common/src/main/java/tech/amak/portbuddy/common/dto/auth/TokenExchangeResponse.java
================================================
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tech.amak.portbuddy.common.dto.auth;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class TokenExchangeResponse {
@JsonProperty("accessToken")
private String accessToken;
@JsonProperty("tokenType")
private String tokenType;
}
================================================
FILE: common/src/main/java/tech/amak/portbuddy/common/dto/jwks/JwkKey.java
================================================
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tech.amak.portbuddy.common.dto.jwks;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class JwkKey {
@JsonProperty("kty")
private String kty;
@JsonProperty("kid")
private String kid;
@JsonProperty("use")
private String use;
@JsonProperty("alg")
private String alg;
// RSA specific
@JsonProperty("n")
private String modulus;
@JsonProperty("e")
private String exponent;
}
================================================
FILE: common/src/main/java/tech/amak/portbuddy/common/dto/jwks/JwksResponse.java
================================================
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tech.amak.portbuddy.common.dto.jwks;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class JwksResponse {
@JsonProperty("keys")
private List<JwkKey> keys;
}
================================================
FILE: common/src/main/java/tech/amak/portbuddy/common/tunnel/BinaryWsFrame.java
================================================
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tech.amak.portbuddy.common.tunnel;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
/**
* Utility to encode/decode binary WebSocket frames for TCP tunneling.
* Frame format (big-endian):
* - 2 bytes: unsigned short representing the byte length of the UTF-8 encoded connectionId (N)
* - N bytes: connectionId UTF-8 bytes
* - R bytes: raw payload data
*/
public final class BinaryWsFrame {
private BinaryWsFrame() {
}
/**
* Encodes the given connection ID and data into a {@link ByteBuffer} following a specific binary
* frame format. The encoded frame contains the connection ID length, the UTF-8 encoded connection ID,
* and a portion of the data starting at the specified offset and up to the specified length.
*
* @param connectionId the connection identifier to be encoded (expected to be non-null)
* @param data the raw payload data to be included in the frame (expected to be non-null)
* @param offset the starting position of the data array to be included
* @param length the number of bytes from the data array to be included
* @return a {@link ByteBuffer} containing the encoded frame data
*/
public static ByteBuffer encodeToByteBuffer(final String connectionId,
final byte[] data,
final int offset,
final int length) {
final var idBytes = connectionId.getBytes(StandardCharsets.UTF_8);
final var capacity = 2 + idBytes.length + length;
final var buffer = ByteBuffer.allocate(capacity);
buffer.putShort((short) (idBytes.length & 0xFFFF));
buffer.put(idBytes);
buffer.put(data, offset, length);
buffer.flip();
return buffer;
}
/**
* Encodes the given connection ID and data into a byte array following a specific binary
* frame format. The encoded frame includes the connection ID length, the UTF-8 encoded
* connection ID, and a portion of the data starting at the specified offset and up to the
* specified length.
*
* @param connectionId the connection identifier to be encoded (expected to be non-null)
* @param data the raw payload data to be included in the frame (expected to be non-null)
* @param offset the starting position of the data array to be included
* @param length the number of bytes from the data array to be included
* @return a byte array containing the encoded frame data
*/
public static byte[] encodeToArray(final String connectionId,
final byte[] data,
final int offset,
final int length) {
final var buffer = encodeToByteBuffer(connectionId, data, offset, length);
final var out = new byte[buffer.remaining()];
buffer.get(out);
buffer.clear();
return out;
}
/**
* Decodes a binary frame from the provided {@link ByteBuffer} into a {@code Decoded} record object.
* The frame is expected to have a specific format, starting with a 2-byte length
* field indicating the UTF-8 encoded connection ID's length, followed by the connection ID bytes,
* and ending with the remaining data bytes.
*
* @param buffer the {@link ByteBuffer} containing the binary frame data to decode;
* must have sufficient remaining bytes to represent a valid frame.
* @return a {@code Decoded} object containing the connection ID and data extracted from the frame,
* or {@code null} if the buffer does not contain a valid or complete frame.
*/
public static Decoded decode(final ByteBuffer buffer) {
if (buffer.remaining() < 2) {
return null;
}
final var length = Short.toUnsignedInt(buffer.getShort());
if (buffer.remaining() < length) {
return null;
}
final var idBytes = new byte[length];
buffer.get(idBytes);
final var connectionId = new String(idBytes, StandardCharsets.UTF_8);
final var data = new byte[buffer.remaining()];
buffer.get(data);
return new Decoded(connectionId, data);
}
/**
* Decodes a binary frame from the provided byte array into a {@code Decoded} record object.
* The frame is expected to have a specific format, starting with a 2-byte length
* field indicating the UTF-8 encoded connection ID's length, followed by the connection ID bytes,
* and ending with the remaining data bytes.
*
* @param frameBytes the byte array containing the binary frame data to decode;
* must have sufficient bytes to represent a valid frame.
* @return a {@code Decoded} object containing the connection ID and data extracted from the frame,
* or {@code null} if the array does not contain a valid or complete frame.
*/
public static Decoded decode(final byte[] frameBytes) {
final var byteBuffer = ByteBuffer.wrap(frameBytes);
final var decoded = decode(byteBuffer);
byteBuffer.clear();
return decoded;
}
/**
* A record that represents the result of decoding a binary WebSocket frame.
* It contains a connection identifier and the corresponding payload data.
*
* <ul>
* <li>The {@code connectionId} represents the unique identifier of the connection.
* <li>The {@code data} represents the raw payload data associated with the frame.
* </ul>
* Instances of this record are typically produced by decoding operations on binary
* WebSocket frames, which follow a specific format.
*/
public record Decoded(String connectionId, byte[] data) {
}
}
================================================
FILE: common/src/main/java/tech/amak/portbuddy/common/tunnel/ControlMessage.java
================================================
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tech.amak.portbuddy.common.tunnel;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* Lightweight control envelope for tunnel health/keep-alive messages.
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ControlMessage {
@JsonProperty("kind")
private final String kind = "CTRL";
@JsonProperty("type")
private Type type;
/**
* Optional unix timestamp millis to help with diagnostics.
*/
@JsonProperty("ts")
private Long ts;
public enum Type {
PING,
PONG,
EXIT
}
}
================================================
FILE: common/src/main/java/tech/amak/portbuddy/common/tunnel/HttpTunnelMessage.java
================================================
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tech.amak.portbuddy.common.tunnel;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* Envelope for HTTP tunnel messages exchanged over WebSocket between server and CLI.
* To keep it simple, messages are whole-request/whole-response with base64 bodies.
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class HttpTunnelMessage {
/**
* Unique ID to correlate request and response.
*/
@JsonProperty("id")
private String id;
/**
* Message type.
*/
@JsonProperty("type")
private Type type;
// Request fields
@JsonProperty("method")
private String method;
@JsonProperty("path")
private String path;
@JsonProperty("query")
private String query;
@JsonProperty("headers")
private Map<String, List<String>> headers;
/**
* Request body encoded as Base64.
*/
@JsonProperty("bodyB64")
private String bodyB64;
/**
* Original request body media type (e.g., "application/json; charset=utf-8").
* The server captures this from the ingress request and the CLI uses it to
* reconstruct the upstream request body with the same Content-Type.
*/
@JsonProperty("bodyContentType")
private String bodyContentType;
// Response fields
@JsonProperty("status")
private Integer status;
@JsonProperty("respHeaders")
private Map<String, List<String>> respHeaders;
/**
* Response body encoded as Base64.
*/
@JsonProperty("respBodyB64")
private String respBodyB64;
public enum Type {
REQUEST,
RESPONSE
}
}
================================================
FILE: common/src/main/java/tech/amak/portbuddy/common/tunnel/MessageEnvelope.java
================================================
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tech.amak.portbuddy.common.tunnel;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* Minimal envelope to route incoming WS messages without using JsonNode.
* If {@code kind} is null, treat it as an HTTP tunnel message.
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class MessageEnvelope {
@JsonProperty("kind")
private String kind; // CTRL, WS or null (HTTP)
}
================================================
FILE: common/src/main/java/tech/amak/portbuddy/common/tunnel/WsTunnelMessage.java
================================================
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tech.amak.portbuddy.common.tunnel;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* Envelope for WebSocket tunneling over the existing control WebSocket between server and CLI.
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class WsTunnelMessage {
/**
* Constant marker to distinguish from HTTP messages.
*/
@JsonProperty("kind")
private final String kind = "WS";
/**
* Correlates messages of the same WS connection.
*/
@JsonProperty("connectionId")
private String connectionId;
/**
* Optional request/response id alignment if needed.
*/
@JsonProperty("id")
private String id;
@JsonProperty("wsType")
private Type wsType;
// For OPEN from server to client
@JsonProperty("path")
private String path;
@JsonProperty("query")
private String query;
@JsonProperty("headers")
private Map<String, String> headers;
// Payload
@JsonProperty("text")
private String text;
@JsonProperty("dataB64")
private String dataB64;
// Close details
@JsonProperty("closeCode")
private Integer closeCode;
@JsonProperty("closeReason")
private String closeReason;
public enum Type {
OPEN,
OPEN_OK,
TEXT,
BINARY,
CLOSE,
ERROR,
/**
* Control message sent by Net Proxy after WebSocket is established to inform CLI
* about the actual exposed public endpoint details (host/port).
*/
EXPOSED
}
// Public endpoint details for EXPOSED message
@JsonProperty("publicHost")
private String publicHost;
@JsonProperty("publicPort")
private Integer publicPort;
}
================================================
FILE: common/src/main/java/tech/amak/portbuddy/common/utils/IdUtils.java
================================================
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tech.amak.portbuddy.common.utils;
import java.net.URI;
import java.util.UUID;
public class IdUtils {
private IdUtils() {
}
/**
* Extracts the tunnel ID from the given URI by taking the last part of the URI path
* and parsing it as a UUID. If the URI is null, its path is null, or the last part
* of the path is not a valid UUID, the method will return null.
*
* @param uri the URI from which the tunnel ID is to be extracted
* @return the UUID extracted from the last part of the URI path, or null if the input is invalid
*/
public static UUID extractTunnelId(final URI uri) {
if (uri == null) {
return null;
}
final var path = uri.getPath();
if (path == null) {
return null;
}
final var parts = path.split("/");
final var idPart = parts.length > 0 ? parts[parts.length - 1] : null;
return idPart == null
? null
: parseUuid(idPart);
}
/**
* Parses a given string into a UUID. If the string cannot be parsed as a valid UUID,
* the method returns null.
*
* @param id the string representation of the UUID to be parsed
* @return the parsed UUID if the input is valid, or null if the input is invalid
*/
public static UUID parseUuid(final String id) {
try {
return UUID.fromString(id);
} catch (final Exception e) {
return null;
}
}
}
================================================
FILE: docker-compose.yml
================================================
services:
postgres:
image: postgres:16-alpine
container_name: pb-db
restart: unless-stopped
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
ports:
- "5432:5432"
volumes:
- ${PWD}/pg_data:/var/lib/postgresql/data
logging:
options:
max-size: 10M
eureka:
image: cr.yandex/crpop0r0f81mcsvg9kei/port-buddy-eureka:latest
container_name: pb-eureka
restart: unless-stopped
network_mode: host
environment:
EUREKA_USERNAME: ${EUREKA_USERNAME}
EUREKA_PASSWORD: ${EUREKA_PASSWORD}
volumes:
- ${PWD}/eureka/config:/app/config
- ${PWD}/eureka/logs:/app/log
logging:
options:
max-size: 10M
server:
image: cr.yandex/crpop0r0f81mcsvg9kei/port-buddy-server:latest
container_name: pb-server
restart: unless-stopped
network_mode: host
environment:
DB_NAME: ${DB_NAME}
DB_USER: ${DB_USER}
DB_PASSWORD: ${DB_PASSWORD}
DB_HOST: ${DB_HOST}
DB_PORT: ${DB_PORT}
SMTP_HOST: ${SMTP_HOST}
SMTP_PORT: ${SMTP_PORT}
SMTP_USERNAME: ${SMTP_USERNAME}
SMTP_PASSWORD: ${SMTP_PASSWORD}
STRIPE_API_KEY: ${STRIPE_API_KEY}
STRIPE_PRICE_PRO: ${STRIPE_PRICE_PRO}
STRIPE_PRICE_TEAM: ${STRIPE_PRICE_TEAM}
STRIPE_PRICE_EXTRA_TUNNEL: ${STRIPE_PRICE_EXTRA_TUNNEL}
STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET}
OAUTH_GOOGLE_CLIENT_ID: ${OAUTH_GOOGLE_CLIENT_ID}
OAUTH_GOOGLE_CLIENT_SECRET: ${OAUTH_GOOGLE_CLIENT_SECRET}
OAUTH_GITHUB_CLIENT_ID: ${OAUTH_GITHUB_CLIENT_ID}
OAUTH_GITHUB_CLIENT_SECRET: ${OAUTH_GITHUB_CLIENT_SECRET}
APP_DOMAIN: ${APP_DOMAIN}
JWT_PUBLIC_KEY: ${JWT_PUBLIC_KEY}
JWT_PRIVATE_KEY: ${JWT_PRIVATE_KEY}
EUREKA_ZONE: ${EUREKA_ZONE}
THREATFOX_ENABLED: ${THREATFOX_ENABLED}
THREATFOX_AUTH_KEY: ${THREATFOX_AUTH_KEY}
JVM_OPTS: -Dspring.profiles.active=prod,default
volumes:
- ${PWD}/server/config:/app/config
- ${PWD}/server/logs:/app/log
- ${PWD}/server/jwt_keys:/app/jwt_keys
depends_on:
- eureka
- postgres
logging:
options:
max-size: 100M
web-app:
image: cr.yandex/crpop0r0f81mcsvg9kei/port-buddy-web-app:latest
container_name: pb-web-app
restart: no
volumes:
- ${PWD}/web-app:/app/pb-web
gateway:
image: cr.yandex/crpop0r0f81mcsvg9kei/port-buddy-gateway:latest
container_name: pb-gateway
restart: unless-stopped
network_mode: host
environment:
APP_DOMAIN: ${APP_DOMAIN}
EUREKA_ZONE: ${EUREKA_ZONE}
JVM_OPTS: -Dspring.profiles.active=prod,default
volumes:
- ${PWD}/gateway/config:/app/config
- ${PWD}/gateway/logs:/app/log
- ${PWD}/ssl-service/certs:/app/certs
- ${PWD}/web-app:/app/web/dist
depends_on:
- web-app
- eureka
- ssl-service
logging:
options:
max-size: 10M
net-proxy:
image: cr.yandex/crpop0r0f81mcsvg9kei/port-buddy-net-proxy:latest
container_name: pb-net-proxy
restart: unless-stopped
network_mode: host
environment:
NET_PROXY_PUBLIC_HOST: ${NET_PROXY_PUBLIC_HOST}
NET_PROXY_REGION: ${NET_PROXY_REGION}
NET_PROXY_COORDINATES: ${NET_PROXY_COORDINATES}
EUREKA_ZONE: ${EUREKA_ZONE}
volumes:
- ${PWD}/net-proxy/config:/app/config
- ${PWD}/net-proxy/logs:/app/log
depends_on:
- eureka
- ssl-service
logging:
options:
max-size: 10M
ssl-service:
image: cr.yandex/crpop0r0f81mcsvg9kei/port-buddy-ssl-service:latest
container_name: pb-ssl-service
restart: unless-stopped
network_mode: host
environment:
DB_NAME: ${DB_NAME}
DB_USER: ${DB_USER}
DB_PASSWORD: ${DB_PASSWORD}
EUREKA_ZONE: ${EUREKA_ZONE}
volumes:
- ${PWD}/ssl-service/config:/app/config
- ${PWD}/ssl-service/logs:/app/log
- ${PWD}/ssl-service/certs:/app/certs
depends_on:
- eureka
- postgres
logging:
options:
max-size: 10M
================================================
FILE: entrypoint-cli-native.sh
================================================
#!/bin/sh
exec /app/portbuddy "$@"
================================================
FILE: entrypoint-web.sh
================================================
#!/bin/sh
echo "Starting" && \
ls -al /app/dist/ && \
mkdir -p /app/pb-web && \
echo "Dir /app/pb-web/ (re)created" && \
rm -rf /app/pb-web/* && \
echo "Dir /app/pb-web/ cleaned" && \
cp -a /app/dist/. /app/pb-web/ && \
echo "Files copied to /app/pb-web/ folder" && \
ls -al /app/pb-web/
================================================
FILE: entrypoint.sh
================================================
#!/bin/sh
exec java $JVM_OPTS -jar /app/app.jar $@
================================================
FILE: eureka/HELP.md
================================================
# Getting Started
### Reference Documentation
For further reference, please consider the following sections:
* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html)
* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/3.5.7/maven-plugin)
* [Create an OCI image](https://docs.spring.io/spring-boot/3.5.7/maven-plugin/build-image.html)
* [Eureka Server](https://docs.spring.io/spring-cloud-netflix/reference/spring-cloud-netflix.html#spring-cloud-eureka-server)
### Guides
The following guides illustrate how to use some features concretely:
* [Service Registration and Discovery with Eureka and Spring Cloud](https://spring.io/guides/gs/service-registration-and-discovery/)
### Maven Parent overrides
Due to Maven's design, elements are inherited from the parent POM to the project POM.
While most of the inheritance is fine, it also inherits unwanted elements like `<license>` and `<developers>` from the parent.
To prevent this, the project POM contains empty overrides for these elements.
If you manually switch to a different parent and actually want the inheritance, you need to remove those overrides.
================================================
FILE: eureka/pom.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>tech.amak</groupId>
<artifactId>port-buddy</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>eureka</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>eureka</name>
<description>Discovery Server</description>
<properties>
<java.version>25</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
================================================
FILE: eureka/src/main/java/tech/amak/portbuddy/eureka/EurekaApplication.java
================================================
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tech.amak.portbuddy.eureka;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@ConfigurationPropertiesScan
@EnableEurekaServer
public class EurekaApplication {
public static void main(final String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
================================================
FILE: eureka/src/main/java/tech/amak/portbuddy/eureka/security/SecurityConfig.java
================================================
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tech.amak.portbuddy.eureka.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
@Order(1)
public SecurityFilterChain apiSecurityFilterChain(final HttpSecurity http) throws Exception {
http.authorizeHttpRequests((authz) -> authz
.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults());
http.csrf(csrf -> csrf.ignoringRequestMatchers("/eureka/**"));
return http.build();
}
}
================================================
FILE: eureka/src/main/resources/application.yml
================================================
server:
port: 8761
eureka:
instance:
hostname: localhost
client:
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka
register-with-eureka: false
fetch-registry: false
spring:
application:
name: eureka
security:
user:
name: ${EUREKA_USERNAME:portbuddy}
password: ${EUREKA_PASSWORD:portbuddy}
logging:
level:
root: warn
file:
name: log/app.log
logback:
rollingpolicy:
max-history: 14
total-size-cap: 100MB
max-file-size: 10MB
================================================
FILE: eureka/src/test/java/tech/amak/portbuddy/eureka/EurekaApplicationTests.java
================================================
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tech.amak.portbuddy.eureka;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class EurekaApplicationTests {
@Test
void contextLoads() {
}
}
================================================
FILE: gateway/HELP.md
================================================
# Getting Started
### Reference Documentation
For further reference, please consider the following sections:
* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html)
* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/3.5.7/maven-plugin)
* [Create an OCI image](https://docs.spring.io/spring-boot/3.5.7/maven-plugin/build-image.html)
* [GraalVM Native Image Support](https://docs.spring.io/spring-boot/3.5.7/reference/packaging/native-image/introducing-graalvm-native-images.html)
* [Spring Configuration Processor](https://docs.spring.io/spring-boot/3.5.7/specification/configuration-metadata/annotation-processor.html)
* [Reactive Gateway](https://docs.spring.io/spring-cloud-gateway/reference/spring-cloud-gateway.html)
### Guides
The following guides illustrate how to use some features concretely:
* [Using Spring Cloud Gateway](https://github.com/spring-cloud-samples/spring-cloud-gateway-sample)
### Additional Links
These additional references should also help you:
* [Configure AOT settings in Build Plugin](https://docs.spring.io/spring-boot/3.5.7/how-to/aot.html)
## GraalVM Native Support
This project has been configured to let you generate either a lightweight container or a native executable.
It is also possible to run your tests in a native image.
### Lightweight Container with Cloud Native Buildpacks
If you're already familiar with Spring Boot container images support, this is the easiest way to get started.
Docker should be installed and configured on your machine prior to creating the image.
To create the image, run the following goal:
```
$ ./mvnw spring-boot:build-image -Pnative
```
Then, you can run the app like any other container:
```
$ docker run --rm gateway:0.0.1-SNAPSHOT
```
### Executable with Native Build Tools
Use this option if you want to explore more options such as running your tests in a native image.
The GraalVM `native-image` compiler should be installed and configured on your machine.
NOTE: GraalVM 22.3+ is required.
To create the executable, run the following goal:
```
$ ./mvnw native:compile -Pnative
```
Then, you can run the app as follows:
```
$ target/gateway
```
You can also run your existing tests suite in a native image.
This is an efficient way to validate the compatibility of your application.
To run your existing tests in a native image, run the following goal:
```
$ ./mvnw test -PnativeTest
```
### Maven Parent overrides
Due to Maven's design, elements are inherited from the parent POM to the project POM.
While most of the inheritance is fine, it also inherits unwanted elements like `<license>` and `<developers>` from the parent.
To prevent this, the project POM contains empty overrides for these elements.
If you manually switch to a different parent and actually want the inheritance, you need to remove those overrides.
================================================
FILE: gateway/pom.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>tech.amak</groupId>
<artifactId>port-buddy</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>gateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>api-gateway</name>
<description>API Gateway to proxy incoming requests to downstream</description>
<properties>
<java.version>25</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway-server-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>1.80</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Inherit maven-compiler-plugin configuration (release, lombok processor) from parent -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
================================================
FILE: gateway/src/main/java/tech/amak/portbuddy/gateway/ApiGatewayApplication.java
================================================
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tech.amak.portbuddy.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
@SpringBootApplication
@ConfigurationPropertiesScan
public class ApiGatewayApplication {
public static void main(final String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
================================================
FILE: gateway/src/main/java/tech/amak/portbuddy/gateway/client/SslServiceClient.java
================================================
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tech.amak.portbuddy.gateway.client;
import java.time.Duration;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Mono;
import tech.amak.portbuddy.gateway.dto.CertificateResponse;
@Service
@Slf4j
public class SslServiceClient {
private final WebClient webClient;
/**
* Constructs an instance of SslServiceClient with a load-balanced WebClient configured
* to interact with the ssl-service.
*
* @param loadBalancedWebClientBuilder the WebClient.Builder instance used to configure
* the load-balanced WebClient for communication with
* the ssl-service
*/
public SslServiceClient(final WebClient.Builder loadBalancedWebClientBuilder) {
this.webClient = loadBalancedWebClientBuilder
.baseUrl("lb://ssl-service")
.build();
}
/**
* Retrieves certificate metadata for a given domain from the ssl-service.
*
* @param domain domain name
* @return certificate response mono
*/
public Mono<CertificateResponse> getCertificate(final String domain) {
return webClient.get()
.uri("/internal/api/certificates/{domain}", domain)
.retrieve()
.bodyToMono(CertificateResponse.class)
.timeout(Duration.ofSeconds(5))
.onErrorResume(e -> {
log.warn("Failed to retrieve certificate for domain [{}]: {}", domain, e.getMessage());
return Mono.empty();
});
}
}
================================================
FILE: gateway/src/main/java/tech/amak/portbuddy/gateway/config/AppProperties.java
================================================
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tech.amak.portbuddy.gateway.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.io.Resource;
@ConfigurationProperties(prefix = "app")
public record AppProperties(
int httpPort,
String domain,
String url,
String serverErrorPage,
Jwt jwt,
Ssl ssl
) {
public record Ssl(
boolean enabled,
Certificate fallback
) {
}
public record Certificate(
boolean enabled,
Resource keyCertChainFile,
Resource keyFile
) {
}
public record Jwt(
String issuer,
String jwkSetUri
) {
}
}
================================================
FILE: gateway/src/main/java/tech/amak/portbuddy/gateway/config/GlobalExceptionHandler.java
================================================
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissio
gitextract_lb0oskn5/
├── .github/
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── .mvn/
│ └── wrapper/
│ └── maven-wrapper.properties
├── Dockerfile-cli
├── Dockerfile-eureka
├── Dockerfile-gateway
├── Dockerfile-net-proxy
├── Dockerfile-server
├── Dockerfile-ssl-service
├── Dockerfile-web
├── LICENSE
├── README.md
├── cli/
│ ├── pom.xml
│ └── src/
│ └── main/
│ ├── java/
│ │ └── tech/
│ │ └── amak/
│ │ └── portbuddy/
│ │ └── cli/
│ │ ├── PortBuddy.java
│ │ ├── config/
│ │ │ └── ConfigurationService.java
│ │ ├── tunnel/
│ │ │ ├── HttpTunnelClient.java
│ │ │ └── NetTunnelClient.java
│ │ ├── ui/
│ │ │ ├── ConsoleUi.java
│ │ │ ├── HttpLogSink.java
│ │ │ └── NetTrafficSink.java
│ │ └── utils/
│ │ ├── HttpUtils.java
│ │ └── JsonUtils.java
│ └── resources/
│ ├── META-INF/
│ │ └── native-image/
│ │ └── tech.amak/
│ │ └── port-buddy-cli/
│ │ ├── reflect-config.json
│ │ └── resource-config.json
│ ├── application-dev.yml
│ ├── application.yml
│ └── logback.xml
├── common/
│ ├── pom.xml
│ └── src/
│ └── main/
│ └── java/
│ └── tech/
│ └── amak/
│ └── portbuddy/
│ └── common/
│ ├── ClientConfig.java
│ ├── Plan.java
│ ├── TunnelType.java
│ ├── dto/
│ │ ├── DnsInstructionsEmailRequest.java
│ │ ├── ExposeRequest.java
│ │ ├── ExposeResponse.java
│ │ ├── auth/
│ │ │ ├── RegisterRequest.java
│ │ │ ├── RegisterResponse.java
│ │ │ ├── TokenExchangeRequest.java
│ │ │ └── TokenExchangeResponse.java
│ │ └── jwks/
│ │ ├── JwkKey.java
│ │ └── JwksResponse.java
│ ├── tunnel/
│ │ ├── BinaryWsFrame.java
│ │ ├── ControlMessage.java
│ │ ├── HttpTunnelMessage.java
│ │ ├── MessageEnvelope.java
│ │ └── WsTunnelMessage.java
│ └── utils/
│ └── IdUtils.java
├── docker-compose.yml
├── entrypoint-cli-native.sh
├── entrypoint-web.sh
├── entrypoint.sh
├── eureka/
│ ├── HELP.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── tech/
│ │ │ └── amak/
│ │ │ └── portbuddy/
│ │ │ └── eureka/
│ │ │ ├── EurekaApplication.java
│ │ │ └── security/
│ │ │ └── SecurityConfig.java
│ │ └── resources/
│ │ └── application.yml
│ └── test/
│ └── java/
│ └── tech/
│ └── amak/
│ └── portbuddy/
│ └── eureka/
│ └── EurekaApplicationTests.java
├── gateway/
│ ├── HELP.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── tech/
│ │ │ └── amak/
│ │ │ └── portbuddy/
│ │ │ └── gateway/
│ │ │ ├── ApiGatewayApplication.java
│ │ │ ├── client/
│ │ │ │ └── SslServiceClient.java
│ │ │ ├── config/
│ │ │ │ ├── AppProperties.java
│ │ │ │ ├── GlobalExceptionHandler.java
│ │ │ │ ├── LoadBalancerClientsConfig.java
│ │ │ │ ├── NetProxyLoadBalancerConfiguration.java
│ │ │ │ ├── PortBuddyServerLoadBalancerConfiguration.java
│ │ │ │ ├── SslServerConfig.java
│ │ │ │ └── WebClientConfig.java
│ │ │ ├── dto/
│ │ │ │ └── CertificateResponse.java
│ │ │ ├── filter/
│ │ │ │ └── PortBuddyRewritePathGatewayFilterFactory.java
│ │ │ ├── loadbalancer/
│ │ │ │ ├── NetProxyPublicHostLoadBalancer.java
│ │ │ │ └── PortBuddySubdomainLoadBalancer.java
│ │ │ ├── security/
│ │ │ │ ├── GatewayJwtConfig.java
│ │ │ │ └── GatewaySecurityConfig.java
│ │ │ └── ssl/
│ │ │ ├── DynamicSslProvider.java
│ │ │ └── SniSslContextMapping.java
│ │ └── resources/
│ │ └── application.yml
│ └── test/
│ └── java/
│ └── tech/
│ └── amak/
│ ├── gateway/
│ │ └── ApiGatewayApplicationTests.java
│ └── portbuddy/
│ └── gateway/
│ ├── config/
│ │ └── SslServerConfigTest.java
│ └── ssl/
│ └── DynamicSslProviderTest.java
├── lombok.config
├── mvnw
├── mvnw.cmd
├── net-proxy/
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── tech/
│ │ │ └── amak/
│ │ │ └── portbuddy/
│ │ │ └── netproxy/
│ │ │ ├── NetProxyApplication.java
│ │ │ ├── config/
│ │ │ │ ├── AppProperties.java
│ │ │ │ ├── JwtConfig.java
│ │ │ │ └── WebSocketConfig.java
│ │ │ ├── security/
│ │ │ │ └── SecurityConfig.java
│ │ │ ├── tunnel/
│ │ │ │ ├── NetTunnelRegistry.java
│ │ │ │ └── NetTunnelWebSocketHandler.java
│ │ │ └── web/
│ │ │ └── NetProxyController.java
│ │ └── resources/
│ │ └── application.yml
│ └── test/
│ └── java/
│ └── tech/
│ └── amak/
│ └── portbuddy/
│ └── netproxy/
│ └── tunnel/
│ ├── NetTunnelLeakVerificationTest.java
│ ├── NetTunnelOrphanCleanupTest.java
│ ├── NetTunnelRegistryConcurrencyTest.java
│ ├── NetTunnelRegistryTest.java
│ └── NetTunnelUdpEvictionTest.java
├── pom.xml
├── server/
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── tech/
│ │ │ └── amak/
│ │ │ └── portbuddy/
│ │ │ └── server/
│ │ │ ├── ServerApplication.java
│ │ │ ├── client/
│ │ │ │ ├── NetProxyClient.java
│ │ │ │ └── SslServiceClient.java
│ │ │ ├── config/
│ │ │ │ ├── AppProperties.java
│ │ │ │ ├── SchedulingConfig.java
│ │ │ │ ├── ThreatFoxProperties.java
│ │ │ │ └── TunnelsProperties.java
│ │ │ ├── db/
│ │ │ │ ├── entity/
│ │ │ │ │ ├── AccountEntity.java
│ │ │ │ │ ├── ApiKeyEntity.java
│ │ │ │ │ ├── DomainEntity.java
│ │ │ │ │ ├── InvitationEntity.java
│ │ │ │ │ ├── PasswordResetTokenEntity.java
│ │ │ │ │ ├── PortReservationEntity.java
│ │ │ │ │ ├── Role.java
│ │ │ │ │ ├── StripeEventEntity.java
│ │ │ │ │ ├── TunnelEntity.java
│ │ │ │ │ ├── TunnelStatus.java
│ │ │ │ │ ├── UserAccountEntity.java
│ │ │ │ │ └── UserEntity.java
│ │ │ │ └── repo/
│ │ │ │ ├── AccountRepository.java
│ │ │ │ ├── ApiKeyRepository.java
│ │ │ │ ├── DomainRepository.java
│ │ │ │ ├── InvitationRepository.java
│ │ │ │ ├── PasswordResetTokenRepository.java
│ │ │ │ ├── PortReservationRepository.java
│ │ │ │ ├── StripeEventRepository.java
│ │ │ │ ├── TunnelRepository.java
│ │ │ │ ├── UserAccountRepository.java
│ │ │ │ └── UserRepository.java
│ │ │ ├── mail/
│ │ │ │ ├── EmailService.java
│ │ │ │ ├── UserCreatedEvent.java
│ │ │ │ └── WelcomeEmailService.java
│ │ │ ├── security/
│ │ │ │ ├── ApiTokenAuthFilter.java
│ │ │ │ ├── JwtConfig.java
│ │ │ │ ├── JwtService.java
│ │ │ │ ├── Oauth2SuccessHandler.java
│ │ │ │ ├── RsaKeyProvider.java
│ │ │ │ ├── SecurityConfig.java
│ │ │ │ └── ThreatBlockedException.java
│ │ │ ├── service/
│ │ │ │ ├── ApiTokenService.java
│ │ │ │ ├── DomainService.java
│ │ │ │ ├── PaymentCleanupService.java
│ │ │ │ ├── PortReservationService.java
│ │ │ │ ├── ProxyDiscoveryService.java
│ │ │ │ ├── StaleTunnelsReaper.java
│ │ │ │ ├── StripeService.java
│ │ │ │ ├── StripeWebhookService.java
│ │ │ │ ├── TeamService.java
│ │ │ │ ├── TunnelService.java
│ │ │ │ ├── threatfox/
│ │ │ │ │ ├── ThreatFoxClient.java
│ │ │ │ │ ├── ThreatFoxIoc.java
│ │ │ │ │ ├── ThreatFoxRequest.java
│ │ │ │ │ ├── ThreatFoxResponse.java
│ │ │ │ │ └── ThreatFoxService.java
│ │ │ │ └── user/
│ │ │ │ ├── MissingEmailException.java
│ │ │ │ ├── PasswordResetService.java
│ │ │ │ └── UserProvisioningService.java
│ │ │ ├── tunnel/
│ │ │ │ ├── PermissiveSubprotocolHandshakeHandler.java
│ │ │ │ ├── PublicWebSocketProxyHandler.java
│ │ │ │ ├── TunnelRegistry.java
│ │ │ │ ├── TunnelWebSocketHandler.java
│ │ │ │ └── WebSocketConfig.java
│ │ │ └── web/
│ │ │ ├── AuthController.java
│ │ │ ├── DomainsController.java
│ │ │ ├── ExposeController.java
│ │ │ ├── IngressController.java
│ │ │ ├── IngressResolveController.java
│ │ │ ├── InternalDomainController.java
│ │ │ ├── InternalEmailController.java
│ │ │ ├── JwksController.java
│ │ │ ├── PaymentController.java
│ │ │ ├── PortsController.java
│ │ │ ├── StripeWebhookController.java
│ │ │ ├── TeamController.java
│ │ │ ├── TokensController.java
│ │ │ ├── TunnelStatusController.java
│ │ │ ├── TunnelsController.java
│ │ │ ├── UsersController.java
│ │ │ ├── admin/
│ │ │ │ ├── AdminAccountController.java
│ │ │ │ ├── AdminSystemController.java
│ │ │ │ ├── AdminTunnelController.java
│ │ │ │ ├── AdminUserController.java
│ │ │ │ └── dto/
│ │ │ │ ├── AdminAccountRow.java
│ │ │ │ ├── AdminStatsRow.java
│ │ │ │ ├── AdminTunnelRow.java
│ │ │ │ ├── AdminUserRow.java
│ │ │ │ └── SystemStatsResponse.java
│ │ │ ├── advice/
│ │ │ │ └── GlobalExceptionHandler.java
│ │ │ └── dto/
│ │ │ ├── DomainDto.java
│ │ │ ├── LoginRequest.java
│ │ │ ├── PasswordResetConfirm.java
│ │ │ ├── PasswordResetRequest.java
│ │ │ ├── PortRangeDto.java
│ │ │ ├── PortReservationDto.java
│ │ │ ├── PortReservationUpdateRequest.java
│ │ │ ├── SetPasscodeRequest.java
│ │ │ ├── UpdateCustomDomainRequest.java
│ │ │ └── UpdateDomainRequest.java
│ │ └── resources/
│ │ ├── application.yml
│ │ ├── db/
│ │ │ └── migration/
│ │ │ ├── V10__link_http_tunnels_to_domain.sql
│ │ │ ├── V11__add_deleted_column_to_domains.sql
│ │ │ ├── V12__drop_tunnel_id_from_tunnels.sql
│ │ │ ├── V13__password_reset_tokens.sql
│ │ │ ├── V14__port_reservations.sql
│ │ │ ├── V15__add_user_to_port_reservations.sql
│ │ │ ├── V16__add_port_reservation_to_tunnels.sql
│ │ │ ├── V17__soft_delete_port_reservations.sql
│ │ │ ├── V18__add_passcode_to_domains.sql
│ │ │ ├── V19__add_temp_passcode_to_tunnels.sql
│ │ │ ├── V1__accounts_and_users.sql
│ │ │ ├── V20__add_custom_domain_to_domains.sql
│ │ │ ├── V21__add_roles_to_users.sql
│ │ │ ├── V22__update_plans.sql
│ │ │ ├── V23__add_stripe_fields.sql
│ │ │ ├── V24__create_stripe_events_table.sql
│ │ │ ├── V25__create_invitations_table.sql
│ │ │ ├── V26__many_to_many_users_accounts.sql
│ │ │ ├── V27__add_ssl_active_to_domains.sql
│ │ │ ├── V28__add_blocked_to_accounts.sql
│ │ │ ├── V29__add_name_to_port_reservations.sql
│ │ │ ├── V2__api_keys.sql
│ │ │ ├── V30__unique_port_reservation_name.sql
│ │ │ ├── V31__lowercase_domains.sql
│ │ │ ├── V3__tunnels.sql
│ │ │ ├── V4__shedlock_and_heartbeat_indexes.sql
│ │ │ ├── V5__add_password_and_admin.sql
│ │ │ ├── V6__create_domains_table.sql
│ │ │ ├── V7__link_tunnels_to_account.sql
│ │ │ ├── V8__add_user_id_to_tunnels.sql
│ │ │ └── V9__link_api_keys_to_account.sql
│ │ ├── keys/
│ │ │ ├── dev_jwt.pem
│ │ │ └── dev_jwt.pub
│ │ └── templates/
│ │ └── email/
│ │ ├── base.html
│ │ ├── dns-instructions.html
│ │ ├── password-reset-success.html
│ │ ├── password-reset.html
│ │ ├── payment-failed.html
│ │ ├── plan-changed.html
│ │ ├── subscription-canceled.html
│ │ ├── subscription-success.html
│ │ ├── team-invite.html
│ │ └── welcome.html
│ └── test/
│ └── java/
│ └── tech/
│ └── amak/
│ └── portbuddy/
│ └── server/
│ ├── security/
│ │ └── Oauth2SuccessHandlerTest.java
│ ├── service/
│ │ ├── DomainServiceTest.java
│ │ ├── PaymentCleanupServiceTest.java
│ │ ├── PortReservationServiceTest.java
│ │ ├── StaleTunnelsReaperTest.java
│ │ └── TunnelServiceTest.java
│ ├── tunnel/
│ │ └── TunnelRegistryLeakTest.java
│ ├── user/
│ │ └── PasswordResetServiceTest.java
│ └── web/
│ ├── AuthControllerTest.java
│ ├── IngressControllerTest.java
│ ├── PaymentControllerTest.java
│ ├── StripeWebhookControllerTest.java
│ ├── TeamControllerTest.java
│ ├── TokensControllerTest.java
│ ├── UsersControllerTest.java
│ └── admin/
│ └── AdminAccountControllerTest.java
├── ssl-service/
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── tech/
│ │ │ └── amak/
│ │ │ └── portbuddy/
│ │ │ └── sslservice/
│ │ │ ├── SslServiceApplication.java
│ │ │ ├── client/
│ │ │ │ └── ServerClient.java
│ │ │ ├── config/
│ │ │ │ ├── AppProperties.java
│ │ │ │ ├── AsyncConfig.java
│ │ │ │ ├── JpaAuditingConfig.java
│ │ │ │ ├── RestConfig.java
│ │ │ │ └── SchedulingConfig.java
│ │ │ ├── domain/
│ │ │ │ ├── CertificateEntity.java
│ │ │ │ ├── CertificateJobEntity.java
│ │ │ │ ├── CertificateJobStatus.java
│ │ │ │ └── CertificateStatus.java
│ │ │ ├── repo/
│ │ │ │ ├── CertificateJobRepository.java
│ │ │ │ └── CertificateRepository.java
│ │ │ ├── security/
│ │ │ │ └── SecurityConfig.java
│ │ │ ├── service/
│ │ │ │ ├── AcmeAccountService.java
│ │ │ │ ├── AcmeCertificateService.java
│ │ │ │ ├── AcmeClientService.java
│ │ │ │ ├── CertificateStorageService.java
│ │ │ │ ├── DnsResolverService.java
│ │ │ │ ├── EmailService.java
│ │ │ │ ├── RenewalScheduler.java
│ │ │ │ ├── RetryExecutor.java
│ │ │ │ ├── TransientErrorClassifier.java
│ │ │ │ └── impl/
│ │ │ │ ├── ServerEmailService.java
│ │ │ │ └── SimpleDnsResolverService.java
│ │ │ ├── web/
│ │ │ │ ├── CertificatesController.java
│ │ │ │ ├── ChallengeController.java
│ │ │ │ ├── InternalController.java
│ │ │ │ ├── JobsController.java
│ │ │ │ └── dto/
│ │ │ │ ├── CreateCertificateRequest.java
│ │ │ │ ├── CreateManagedCertificateRequest.java
│ │ │ │ └── CreateRootDomainRequest.java
│ │ │ └── work/
│ │ │ └── ChallengeTokenStore.java
│ │ └── resources/
│ │ ├── application.yml
│ │ └── db/
│ │ └── migration/
│ │ ├── V1__init.sql
│ │ ├── V2__shedlock_table.sql
│ │ └── V3__add_full_chain_path.sql
│ └── test/
│ ├── java/
│ │ └── tech/
│ │ └── amak/
│ │ └── portbuddy/
│ │ └── sslservice/
│ │ └── service/
│ │ └── CertificateRenewalServiceTest.java
│ └── resources/
│ └── application-test.yml
└── web/
├── index.html
├── package.json
├── pom.xml
├── postcss.config.js
├── public/
│ ├── install.ps1
│ ├── install.sh
│ ├── pages/
│ │ ├── contacts.html
│ │ ├── docs/
│ │ │ └── guides/
│ │ │ ├── hytale-server.html
│ │ │ └── minecraft-server.html
│ │ ├── docs.html
│ │ ├── index.html
│ │ ├── install.html
│ │ ├── privacy.html
│ │ └── terms.html
│ ├── robots.txt
│ ├── setup-portbuddy-service.ps1
│ ├── setup-portbuddy-service.sh
│ ├── site.webmanifest
│ └── sitemap.xml
├── src/
│ ├── App.tsx
│ ├── auth/
│ │ └── AuthContext.tsx
│ ├── components/
│ │ ├── AppLayout.tsx
│ │ ├── CodeBlock.tsx
│ │ ├── LoadingContext.tsx
│ │ ├── Modal.tsx
│ │ ├── PageHeader.tsx
│ │ ├── PlanComparison.tsx
│ │ ├── ProgressBar.tsx
│ │ └── ProtectedRoute.tsx
│ ├── index.css
│ ├── lib/
│ │ ├── api.ts
│ │ └── utils.ts
│ ├── main.tsx
│ └── pages/
│ ├── AcceptInvite.tsx
│ ├── Contacts.tsx
│ ├── ForgotPassword.tsx
│ ├── Installation.tsx
│ ├── Landing.tsx
│ ├── Login.tsx
│ ├── NotFound.tsx
│ ├── Passcode.tsx
│ ├── Privacy.tsx
│ ├── Register.tsx
│ ├── ResetPassword.tsx
│ ├── ServerError.tsx
│ ├── Terms.tsx
│ ├── app/
│ │ ├── AdminAccounts.tsx
│ │ ├── AdminPanel.tsx
│ │ ├── AdminTunnels.tsx
│ │ ├── AdminUsers.tsx
│ │ ├── Billing.tsx
│ │ ├── BillingCancel.tsx
│ │ ├── BillingSuccess.tsx
│ │ ├── Domains.tsx
│ │ ├── Ports.tsx
│ │ ├── Profile.tsx
│ │ ├── Settings.tsx
│ │ ├── Team.tsx
│ │ ├── Tokens.tsx
│ │ └── Tunnels.tsx
│ └── docs/
│ ├── DocsLayout.tsx
│ ├── DocsOverview.tsx
│ └── guides/
│ ├── HytaleGuide.tsx
│ └── MinecraftGuide.tsx
├── tailwind.config.js
├── tsconfig.json
└── vite.config.ts
SYMBOL INDEX (992 symbols across 246 files)
FILE: cli/src/main/java/tech/amak/portbuddy/cli/PortBuddy.java
class PortBuddy (line 48) | @Slf4j
method main (line 71) | public static void main(final String[] args) {
method execute (line 83) | public int execute(final String[] args) {
method printHelp (line 149) | private void printHelp() {
method printVersion (line 172) | private void printVersion() {
method init (line 176) | private int init(final String apiToken) {
method expose (line 187) | private int expose() {
method callExposeTunnel (line 327) | private ExposeResponse callExposeTunnel(final String baseUrl, final St...
method exchangeApiTokenForJwt (line 366) | private String exchangeApiTokenForJwt(final String baseUrl, final Stri...
method resolveCliVersion (line 403) | private String resolveCliVersion() {
method ensureAuthenticated (line 412) | private boolean ensureAuthenticated(final ClientConfig config) {
method registerUser (line 449) | private String registerUser(final String baseUrl,
method parseHostPort (line 487) | private HostPort parseHostPort(final String arg) {
class HostPort (line 574) | private static final class HostPort {
method HostPort (line 579) | private HostPort(final String host, final int port, final String sch...
FILE: cli/src/main/java/tech/amak/portbuddy/cli/config/ConfigurationService.java
class ConfigurationService (line 36) | @Slf4j
method ConfigurationService (line 50) | private ConfigurationService() {
method configureLogging (line 61) | private void configureLogging() {
method loadConfig (line 75) | private void loadConfig() throws IOException {
method loadToken (line 98) | private void loadToken() throws IOException {
method getConfig (line 108) | public ClientConfig getConfig() {
method isDev (line 112) | public boolean isDev() {
method saveApiToken (line 126) | public void saveApiToken(final String token) throws IOException {
FILE: cli/src/main/java/tech/amak/portbuddy/cli/tunnel/HttpTunnelClient.java
class HttpTunnelClient (line 55) | @Slf4j
method createHttpClient (line 74) | private static OkHttpClient createHttpClient() {
method createLocalHttpClient (line 87) | private static OkHttpClient createLocalHttpClient() {
method runBlocking (line 137) | public void runBlocking() {
method close (line 195) | public void close() {
method toWebSocketUrl (line 221) | private String toWebSocketUrl(final String base, final String path) {
class Listener (line 233) | private class Listener extends WebSocketListener {
method onOpen (line 234) | @Override
method onMessage (line 259) | @Override
method onClosed (line 311) | @Override
method onFailure (line 329) | @Override
method handleWsFromServer (line 348) | private void handleWsFromServer(final WsTunnelMessage message) {
class LocalWsListener (line 402) | @RequiredArgsConstructor
method onOpen (line 407) | @Override
method onMessage (line 419) | @Override
method onMessage (line 432) | @Override
method onClosed (line 445) | @Override
method onFailure (line 460) | @Override
method handleRequest (line 466) | private HttpTunnelMessage handleRequest(final HttpTunnelMessage reques...
method buildErrorMessage (line 553) | private static HttpTunnelMessage buildErrorMessage(final String id, fi...
method buildBody (line 565) | private RequestBody buildBody(final String method, final String bodyB6...
method methodSupportsBody (line 579) | private boolean methodSupportsBody(final String method) {
method extractHeaders (line 589) | private Map<String, List<String>> extractHeaders(final Response respon...
FILE: cli/src/main/java/tech/amak/portbuddy/cli/tunnel/NetTunnelClient.java
class NetTunnelClient (line 58) | @Slf4j
method runBlocking (line 110) | public void runBlocking() {
method close (line 176) | public void close() {
method close (line 201) | private void close(final LocalTcp localTcp) {
method close (line 211) | private void close(final LocalUdp localUdp) {
method toWebSocketUrl (line 221) | private String toWebSocketUrl(final String httpUri, final String path) {
class Listener (line 234) | private class Listener extends WebSocketListener {
method onOpen (line 235) | @Override
method onMessage (line 287) | @Override
method onMessage (line 316) | @Override
method onClosed (line 369) | @Override
method onFailure (line 388) | @Override
method reportClosedSafe (line 408) | private void reportClosedSafe() {
method handleControl (line 418) | private void handleControl(final WsTunnelMessage message) throws Excep...
method pumpLocalToProxy (line 509) | private void pumpLocalToProxy(final LocalTcp local) {
class LocalTcp (line 540) | private static class LocalTcp {
method LocalTcp (line 546) | LocalTcp(final String connectionId, final Socket sock) throws Except...
class LocalUdp (line 554) | private static class LocalUdp {
method LocalUdp (line 558) | LocalUdp(final String connectionId, final DatagramSocket sock) {
method pumpUdpLocalToProxy (line 564) | private void pumpUdpLocalToProxy(final LocalUdp local) {
method postStatus (line 586) | private void postStatus(final String path) throws Exception {
FILE: cli/src/main/java/tech/amak/portbuddy/cli/ui/ConsoleUi.java
class ConsoleUi (line 40) | @Slf4j
method promptForUserRegistration (line 73) | public static RegisterRequest promptForUserRegistration() throws IOExc...
method isValidEmail (line 92) | private static boolean isValidEmail(final String email) {
method buildTerminal (line 96) | private static Terminal buildTerminal() throws IOException {
method start (line 122) | public void start() {
method waitForExit (line 168) | public void waitForExit() {
method stop (line 187) | public void stop() {
method onHttpLog (line 201) | @Override
method onBytesIn (line 211) | @Override
method onBytesOut (line 216) | @Override
method renderLoop (line 221) | private void renderLoop() {
method clear (line 238) | private void clear() {
method render (line 247) | private void render() {
method safe (line 270) | private String safe(final String value) {
FILE: cli/src/main/java/tech/amak/portbuddy/cli/ui/HttpLogSink.java
type HttpLogSink (line 17) | public interface HttpLogSink {
method onHttpLog (line 18) | void onHttpLog(final String method, final String url, final int status);
FILE: cli/src/main/java/tech/amak/portbuddy/cli/ui/NetTrafficSink.java
type NetTrafficSink (line 17) | public interface NetTrafficSink {
method onBytesIn (line 19) | void onBytesIn(final long bytes);
method onBytesOut (line 21) | void onBytesOut(final long bytes);
FILE: cli/src/main/java/tech/amak/portbuddy/cli/utils/HttpUtils.java
class HttpUtils (line 28) | @Slf4j
method createClient (line 38) | public static OkHttpClient createClient() {
method configureInsecureSsl (line 52) | public static void configureInsecureSsl(final OkHttpClient.Builder bui...
FILE: cli/src/main/java/tech/amak/portbuddy/cli/utils/JsonUtils.java
class JsonUtils (line 23) | @NoArgsConstructor(access = AccessLevel.PRIVATE)
FILE: common/src/main/java/tech/amak/portbuddy/common/ClientConfig.java
class ClientConfig (line 22) | @Data
FILE: common/src/main/java/tech/amak/portbuddy/common/Plan.java
type Plan (line 18) | public enum Plan {
FILE: common/src/main/java/tech/amak/portbuddy/common/TunnelType.java
type TunnelType (line 20) | public enum TunnelType {
method from (line 34) | public static TunnelType from(final String mode) {
FILE: common/src/main/java/tech/amak/portbuddy/common/dto/DnsInstructionsEmailRequest.java
class DnsInstructionsEmailRequest (line 26) | @Data
FILE: common/src/main/java/tech/amak/portbuddy/common/dto/auth/RegisterRequest.java
class RegisterRequest (line 24) | @Data
FILE: common/src/main/java/tech/amak/portbuddy/common/dto/auth/RegisterResponse.java
class RegisterResponse (line 24) | @Data
FILE: common/src/main/java/tech/amak/portbuddy/common/dto/auth/TokenExchangeRequest.java
class TokenExchangeRequest (line 24) | @Data
FILE: common/src/main/java/tech/amak/portbuddy/common/dto/auth/TokenExchangeResponse.java
class TokenExchangeResponse (line 24) | @Data
FILE: common/src/main/java/tech/amak/portbuddy/common/dto/jwks/JwkKey.java
class JwkKey (line 24) | @Data
FILE: common/src/main/java/tech/amak/portbuddy/common/dto/jwks/JwksResponse.java
class JwksResponse (line 26) | @Data
FILE: common/src/main/java/tech/amak/portbuddy/common/tunnel/BinaryWsFrame.java
class BinaryWsFrame (line 27) | public final class BinaryWsFrame {
method BinaryWsFrame (line 29) | private BinaryWsFrame() {
method encodeToByteBuffer (line 43) | public static ByteBuffer encodeToByteBuffer(final String connectionId,
method encodeToArray (line 69) | public static byte[] encodeToArray(final String connectionId,
method decode (line 91) | public static Decoded decode(final ByteBuffer buffer) {
method decode (line 118) | public static Decoded decode(final byte[] frameBytes) {
FILE: common/src/main/java/tech/amak/portbuddy/common/tunnel/ControlMessage.java
class ControlMessage (line 25) | @Data
type Type (line 41) | public enum Type {
FILE: common/src/main/java/tech/amak/portbuddy/common/tunnel/HttpTunnelMessage.java
class HttpTunnelMessage (line 29) | @Data
type Type (line 85) | public enum Type {
FILE: common/src/main/java/tech/amak/portbuddy/common/tunnel/MessageEnvelope.java
class MessageEnvelope (line 26) | @Data
FILE: common/src/main/java/tech/amak/portbuddy/common/tunnel/WsTunnelMessage.java
class WsTunnelMessage (line 27) | @Data
type Type (line 76) | public enum Type {
FILE: common/src/main/java/tech/amak/portbuddy/common/utils/IdUtils.java
class IdUtils (line 20) | public class IdUtils {
method IdUtils (line 22) | private IdUtils() {
method extractTunnelId (line 33) | public static UUID extractTunnelId(final URI uri) {
method parseUuid (line 55) | public static UUID parseUuid(final String id) {
FILE: eureka/src/main/java/tech/amak/portbuddy/eureka/EurekaApplication.java
class EurekaApplication (line 22) | @SpringBootApplication
method main (line 27) | public static void main(final String[] args) {
FILE: eureka/src/main/java/tech/amak/portbuddy/eureka/security/SecurityConfig.java
class SecurityConfig (line 26) | @Configuration
method apiSecurityFilterChain (line 31) | @Bean
FILE: eureka/src/test/java/tech/amak/portbuddy/eureka/EurekaApplicationTests.java
class EurekaApplicationTests (line 20) | @SpringBootTest
method contextLoads (line 23) | @Test
FILE: gateway/src/main/java/tech/amak/portbuddy/gateway/ApiGatewayApplication.java
class ApiGatewayApplication (line 21) | @SpringBootApplication
method main (line 25) | public static void main(final String[] args) {
FILE: gateway/src/main/java/tech/amak/portbuddy/gateway/client/SslServiceClient.java
class SslServiceClient (line 26) | @Service
method SslServiceClient (line 40) | public SslServiceClient(final WebClient.Builder loadBalancedWebClientB...
method getCertificate (line 52) | public Mono<CertificateResponse> getCertificate(final String domain) {
FILE: gateway/src/main/java/tech/amak/portbuddy/gateway/config/GlobalExceptionHandler.java
class GlobalExceptionHandler (line 31) | @ControllerAdvice
method handleIllegalArgumentException (line 38) | @ExceptionHandler(ResponseStatusException.class)
method handleGenericException (line 52) | @ExceptionHandler(Exception.class)
FILE: gateway/src/main/java/tech/amak/portbuddy/gateway/config/LoadBalancerClientsConfig.java
class LoadBalancerClientsConfig (line 24) | @Configuration
FILE: gateway/src/main/java/tech/amak/portbuddy/gateway/config/NetProxyLoadBalancerConfiguration.java
class NetProxyLoadBalancerConfiguration (line 27) | @Slf4j
method reactorServiceInstanceLoadBalancer (line 30) | @Bean
FILE: gateway/src/main/java/tech/amak/portbuddy/gateway/config/PortBuddyServerLoadBalancerConfiguration.java
class PortBuddyServerLoadBalancerConfiguration (line 27) | @Slf4j
method reactorServiceInstanceLoadBalancer (line 30) | @Bean
FILE: gateway/src/main/java/tech/amak/portbuddy/gateway/config/SslServerConfig.java
class SslServerConfig (line 37) | @Configuration
method sslCustomizer (line 53) | @Bean
method startHttpServer (line 95) | @PostConstruct
method stopHttpServer (line 137) | @PreDestroy
FILE: gateway/src/main/java/tech/amak/portbuddy/gateway/config/WebClientConfig.java
class WebClientConfig (line 22) | @Configuration
method loadBalancedWebClientBuilder (line 25) | @Bean
FILE: gateway/src/main/java/tech/amak/portbuddy/gateway/filter/PortBuddyRewritePathGatewayFilterFactory.java
class PortBuddyRewritePathGatewayFilterFactory (line 33) | @Component
method apply (line 36) | @Override
FILE: gateway/src/main/java/tech/amak/portbuddy/gateway/loadbalancer/NetProxyPublicHostLoadBalancer.java
class NetProxyPublicHostLoadBalancer (line 44) | public class NetProxyPublicHostLoadBalancer implements ReactorServiceIns...
method NetProxyPublicHostLoadBalancer (line 58) | public NetProxyPublicHostLoadBalancer(final ObjectProvider<ServiceInst...
method choose (line 64) | @Override
method findByPublicHost (line 91) | private ServiceInstance findByPublicHost(final List<ServiceInstance> i...
method extractRequestedPublicHost (line 105) | private String extractRequestedPublicHost(final Request request) {
FILE: gateway/src/main/java/tech/amak/portbuddy/gateway/loadbalancer/PortBuddySubdomainLoadBalancer.java
class PortBuddySubdomainLoadBalancer (line 44) | public class PortBuddySubdomainLoadBalancer implements ReactorServiceIns...
method PortBuddySubdomainLoadBalancer (line 57) | public PortBuddySubdomainLoadBalancer(final ObjectProvider<ServiceInst...
method choose (line 65) | @Override
method findOwningInstance (line 98) | private Mono<ServiceInstance> findOwningInstance(final List<ServiceIns...
method checkInstance (line 108) | private Mono<ServiceInstance> checkInstance(final ServiceInstance inst...
method extractHost (line 123) | private String extractHost(final Request request) {
method isCustomDomain (line 140) | private boolean isCustomDomain(final String host) {
method extractSubdomain (line 146) | private String extractSubdomain(final String host) {
FILE: gateway/src/main/java/tech/amak/portbuddy/gateway/security/GatewayJwtConfig.java
class GatewayJwtConfig (line 29) | @Configuration
method reactiveJwtDecoder (line 36) | @Bean
FILE: gateway/src/main/java/tech/amak/portbuddy/gateway/security/GatewaySecurityConfig.java
class GatewaySecurityConfig (line 29) | @Configuration
method springSecurityFilterChain (line 33) | @Bean
method corsConfigurationSource (line 65) | @Bean
FILE: gateway/src/main/java/tech/amak/portbuddy/gateway/ssl/DynamicSslProvider.java
class DynamicSslProvider (line 40) | @Service
method DynamicSslProvider (line 57) | public DynamicSslProvider(final SslServiceClient sslServiceClient, fin...
method releaseSslContext (line 77) | private void releaseSslContext(final Object context, final String key) {
method shutdown (line 87) | @PreDestroy
method createFallbackSslContext (line 97) | private SslContext createFallbackSslContext() {
method getSslContext (line 132) | public Mono<SslContext> getSslContext(final String hostname) {
method loadSslContext (line 141) | private Mono<SslContext> loadSslContext(final String hostname) {
FILE: gateway/src/main/java/tech/amak/portbuddy/gateway/ssl/SniSslContextMapping.java
class SniSslContextMapping (line 26) | @Component
method map (line 33) | @Override
FILE: gateway/src/test/java/tech/amak/gateway/ApiGatewayApplicationTests.java
class ApiGatewayApplicationTests (line 24) | @SpringBootTest(
method contextLoads (line 33) | @Test
FILE: gateway/src/test/java/tech/amak/portbuddy/gateway/config/SslServerConfigTest.java
class SslServerConfigTest (line 40) | class SslServerConfigTest {
method shouldInvokeDynamicSslProviderOnSniHandshake (line 42) | @Test
FILE: gateway/src/test/java/tech/amak/portbuddy/gateway/ssl/DynamicSslProviderTest.java
class DynamicSslProviderTest (line 38) | @ExtendWith(MockitoExtension.class)
method setUp (line 55) | @BeforeEach
method shouldReturnFallbackWhenCertificateNotFound (line 63) | @Test
method shouldReturnFallbackWhenHostnameIsNull (line 75) | @Test
method shouldLoadSslContextFromFile (line 84) | @Test
FILE: net-proxy/src/main/java/tech/amak/portbuddy/netproxy/NetProxyApplication.java
class NetProxyApplication (line 25) | @Slf4j
method main (line 30) | static void main(final String[] args) {
method onStart (line 34) | @Bean
FILE: net-proxy/src/main/java/tech/amak/portbuddy/netproxy/config/JwtConfig.java
class JwtConfig (line 32) | @Configuration
method loadBalancedRestTemplate (line 38) | @Bean
method jwtDecoder (line 50) | @Bean
FILE: net-proxy/src/main/java/tech/amak/portbuddy/netproxy/config/WebSocketConfig.java
class WebSocketConfig (line 27) | @Configuration
method registerWebSocketHandlers (line 35) | @Override
method websocketContainer (line 45) | @Bean
FILE: net-proxy/src/main/java/tech/amak/portbuddy/netproxy/security/SecurityConfig.java
class SecurityConfig (line 32) | @Configuration
method apiSecurityFilterChain (line 39) | @Bean
method otherSecurityFilterChain (line 61) | @Bean
method jwtAuthenticationConverter (line 74) | @Bean
FILE: net-proxy/src/main/java/tech/amak/portbuddy/netproxy/tunnel/NetTunnelRegistry.java
class NetTunnelRegistry (line 55) | @Slf4j
method NetTunnelRegistry (line 108) | public NetTunnelRegistry(final ObjectMapper mapper, final AppPropertie...
method shutdown (line 117) | @PreDestroy
method cleanupOrphanedTunnels (line 143) | private void cleanupOrphanedTunnels() {
method closeTunnelUsingPort (line 174) | private void closeTunnelUsingPort(final int port) {
method expose (line 195) | public ExposedPort expose(final UUID tunnelId, final TunnelType tunnel...
method exposeTcp (line 213) | private ExposedPort exposeTcp(final UUID tunnelId, final Integer desir...
method exposeUdp (line 245) | private ExposedPort exposeUdp(final UUID tunnelId, final int desiredPo...
method attachSession (line 283) | public void attachSession(final UUID tunnelId, final WebSocketSession ...
method getSession (line 307) | public WebSocketSession getSession(final UUID tunnelId) {
method detachSession (line 318) | public void detachSession(final WebSocketSession session) {
method closeTunnel (line 334) | public void closeTunnel(final UUID tunnelId) {
method acceptLoop (line 390) | private void acceptLoop(final Tunnel tunnel) {
method handleNewConnection (line 408) | private void handleNewConnection(final Tunnel tunnel, final Socket soc...
method pumpFromPublic (line 455) | private void pumpFromPublic(final Tunnel tunnel, final Connection conn...
method startsWith (line 500) | private boolean startsWith(final byte[] buffer, final int bytesRead, f...
method udpReceiveLoop (line 512) | private void udpReceiveLoop(final Tunnel tunnel) {
method onClientOpenOk (line 534) | public void onClientOpenOk(final UUID tunnelId, final String connectio...
method onClientBinary (line 563) | public void onClientBinary(final UUID tunnelId, final String connectio...
method onClientBinaryBytes (line 589) | public void onClientBinaryBytes(final UUID tunnelId, final String conn...
method onClientClose (line 635) | public void onClientClose(final UUID tunnelId, final String connection...
method sendOpen (line 651) | private boolean sendOpen(final Tunnel tunnel, final String connId) {
method sendToClient (line 658) | private boolean sendToClient(final Tunnel tunnel, final WsTunnelMessag...
method sendBinaryToClient (line 670) | private boolean sendBinaryToClient(final Tunnel tunnel,
class ExposedPort (line 688) | @Data
class Tunnel (line 693) | @Data
method removeEldestEntry (line 705) | @Override
method Tunnel (line 712) | Tunnel(final UUID tunnelId) {
class Connection (line 717) | @Data
method Connection (line 727) | Connection(final String connectionId,
method setCleanupTask (line 737) | void setCleanupTask(final ScheduledFuture<?> cleanupTask) {
method close (line 744) | void close() {
FILE: net-proxy/src/main/java/tech/amak/portbuddy/netproxy/tunnel/NetTunnelWebSocketHandler.java
class NetTunnelWebSocketHandler (line 41) | @Slf4j
method afterConnectionEstablished (line 50) | @Override
method handleTextMessage (line 108) | @Override
method handleBinaryMessage (line 143) | @Override
method afterConnectionClosed (line 155) | @Override
method extractTunnelId (line 170) | private UUID extractTunnelId(final WebSocketSession session) {
method parseQueryParams (line 174) | private Map<String, String> parseQueryParams(final URI uri) {
FILE: net-proxy/src/main/java/tech/amak/portbuddy/netproxy/web/NetProxyController.java
class NetProxyController (line 31) | @RestController
method expose (line 39) | @PostMapping("/expose")
method close (line 54) | @PostMapping("/close")
FILE: net-proxy/src/test/java/tech/amak/portbuddy/netproxy/tunnel/NetTunnelLeakVerificationTest.java
class NetTunnelLeakVerificationTest (line 46) | class NetTunnelLeakVerificationTest {
method testConnectionCleanupOnSendFailure (line 61) | @Test
method testExecutorShutdown (line 106) | @Test
FILE: net-proxy/src/test/java/tech/amak/portbuddy/netproxy/tunnel/NetTunnelOrphanCleanupTest.java
class NetTunnelOrphanCleanupTest (line 32) | class NetTunnelOrphanCleanupTest {
method testOrphanedTunnelCleanup (line 47) | @Test
FILE: net-proxy/src/test/java/tech/amak/portbuddy/netproxy/tunnel/NetTunnelRegistryConcurrencyTest.java
class NetTunnelRegistryConcurrencyTest (line 38) | @Slf4j
method testManyConcurrentConnections (line 54) | @Test
FILE: net-proxy/src/test/java/tech/amak/portbuddy/netproxy/tunnel/NetTunnelRegistryTest.java
class NetTunnelRegistryTest (line 40) | class NetTunnelRegistryTest {
method testBlockHttpOnTcpTunnel (line 55) | @Test
method testAllowNonHttpOnTcpTunnel (line 97) | @Test
method testAllowPostgresSslRequest (line 120) | @Test
method testConnectionWithNoDataSentInitially (line 145) | @Test
FILE: net-proxy/src/test/java/tech/amak/portbuddy/netproxy/tunnel/NetTunnelUdpEvictionTest.java
class NetTunnelUdpEvictionTest (line 47) | class NetTunnelUdpEvictionTest {
method testUdpRemoteLruEviction (line 62) | @Test
FILE: server/src/main/java/tech/amak/portbuddy/server/ServerApplication.java
class ServerApplication (line 23) | @SpringBootApplication
method main (line 29) | public static void main(final String[] args) {
FILE: server/src/main/java/tech/amak/portbuddy/server/client/NetProxyClient.java
type NetProxyClient (line 31) | @FeignClient(
method exposePort (line 37) | @PostMapping("/api/net-proxy/expose")
method closeTunnel (line 42) | @PostMapping("/api/net-proxy/close")
class Configuration (line 45) | class Configuration {
method authorizationHeaderForwarder (line 47) | @Bean
FILE: server/src/main/java/tech/amak/portbuddy/server/client/SslServiceClient.java
type SslServiceClient (line 28) | @FeignClient(
method submitJob (line 34) | @PostMapping("/api/certificates/jobs")
class Configuration (line 39) | class Configuration {
method authorizationHeaderForwarder (line 41) | @Bean
FILE: server/src/main/java/tech/amak/portbuddy/server/config/AppProperties.java
method subdomainHost (line 55) | public String subdomainHost() {
FILE: server/src/main/java/tech/amak/portbuddy/server/config/SchedulingConfig.java
class SchedulingConfig (line 27) | @Configuration
method lockProvider (line 32) | @Bean
FILE: server/src/main/java/tech/amak/portbuddy/server/config/TunnelsProperties.java
class TunnelsProperties (line 26) | @Getter
FILE: server/src/main/java/tech/amak/portbuddy/server/db/entity/AccountEntity.java
class AccountEntity (line 38) | @Getter
FILE: server/src/main/java/tech/amak/portbuddy/server/db/entity/ApiKeyEntity.java
class ApiKeyEntity (line 30) | @Getter
FILE: server/src/main/java/tech/amak/portbuddy/server/db/entity/DomainEntity.java
class DomainEntity (line 38) | @Getter
method toLowerCase (line 84) | @PrePersist
FILE: server/src/main/java/tech/amak/portbuddy/server/db/entity/InvitationEntity.java
class InvitationEntity (line 33) | @Getter
FILE: server/src/main/java/tech/amak/portbuddy/server/db/entity/PasswordResetTokenEntity.java
class PasswordResetTokenEntity (line 33) | @Getter
FILE: server/src/main/java/tech/amak/portbuddy/server/db/entity/PortReservationEntity.java
class PortReservationEntity (line 36) | @Getter
FILE: server/src/main/java/tech/amak/portbuddy/server/db/entity/Role.java
type Role (line 20) | public enum Role {
FILE: server/src/main/java/tech/amak/portbuddy/server/db/entity/StripeEventEntity.java
class StripeEventEntity (line 27) | @Getter
FILE: server/src/main/java/tech/amak/portbuddy/server/db/entity/TunnelEntity.java
class TunnelEntity (line 37) | @Getter
FILE: server/src/main/java/tech/amak/portbuddy/server/db/entity/TunnelStatus.java
type TunnelStatus (line 18) | public enum TunnelStatus {
FILE: server/src/main/java/tech/amak/portbuddy/server/db/entity/UserAccountEntity.java
class UserAccountEntity (line 44) | @Getter
method UserAccountEntity (line 88) | public UserAccountEntity(final UserEntity user, final AccountEntity ac...
method getEmail (line 101) | public String getEmail() {
method getFirstName (line 110) | public String getFirstName() {
method getLastName (line 119) | public String getLastName() {
class UserAccountId (line 123) | @Getter
FILE: server/src/main/java/tech/amak/portbuddy/server/db/entity/UserEntity.java
class UserEntity (line 34) | @Getter
FILE: server/src/main/java/tech/amak/portbuddy/server/db/repo/AccountRepository.java
type AccountRepository (line 30) | public interface AccountRepository extends JpaRepository<AccountEntity, ...
method findByStripeCustomerId (line 31) | Optional<AccountEntity> findByStripeCustomerId(String stripeCustomerId);
method findBySubscriptionStatusNotActiveAndUpdatedAtBefore (line 33) | @Query("SELECT a FROM AccountEntity a WHERE a.subscriptionStatus <> 'a...
method findAdminAccounts (line 36) | @Query(value = """
method findDailyStats (line 54) | @Query(value = """
FILE: server/src/main/java/tech/amak/portbuddy/server/db/repo/ApiKeyRepository.java
type ApiKeyRepository (line 25) | public interface ApiKeyRepository extends JpaRepository<ApiKeyEntity, UU...
method findAllByUserId (line 27) | List<ApiKeyEntity> findAllByUserId(UUID userId);
method findAllByAccountId (line 29) | List<ApiKeyEntity> findAllByAccountId(UUID accountId);
method findByIdAndUserId (line 31) | Optional<ApiKeyEntity> findByIdAndUserId(UUID id, UUID userId);
method findByIdAndAccountId (line 33) | Optional<ApiKeyEntity> findByIdAndAccountId(UUID id, UUID accountId);
method findByTokenHashAndRevokedFalse (line 35) | Optional<ApiKeyEntity> findByTokenHashAndRevokedFalse(String tokenHash);
FILE: server/src/main/java/tech/amak/portbuddy/server/db/repo/DomainRepository.java
type DomainRepository (line 29) | @Repository
method existsBySubdomain (line 31) | boolean existsBySubdomain(String subdomain);
method existsBySubdomainGlobal (line 33) | @Query(value = "SELECT count(*) > 0 FROM domains WHERE subdomain = LOW...
method findAllByAccount (line 36) | List<DomainEntity> findAllByAccount(AccountEntity account);
method findByAccountAndSubdomain (line 38) | Optional<DomainEntity> findByAccountAndSubdomain(AccountEntity account...
method findByIdAndAccount (line 40) | Optional<DomainEntity> findByIdAndAccount(UUID id, AccountEntity accou...
method findBySubdomain (line 42) | Optional<DomainEntity> findBySubdomain(String subdomain);
method findByCustomDomain (line 44) | Optional<DomainEntity> findByCustomDomain(String customDomain);
method countByAccount (line 46) | long countByAccount(AccountEntity account);
method countByAccountAndCustomDomainIsNotNull (line 48) | long countByAccountAndCustomDomainIsNotNull(AccountEntity account);
FILE: server/src/main/java/tech/amak/portbuddy/server/db/repo/InvitationRepository.java
type InvitationRepository (line 26) | public interface InvitationRepository extends JpaRepository<InvitationEn...
method findByToken (line 28) | Optional<InvitationEntity> findByToken(String token);
method findAllByAccountAndAcceptedAtIsNull (line 30) | List<InvitationEntity> findAllByAccountAndAcceptedAtIsNull(AccountEnti...
method findByAccountAndEmailAndAcceptedAtIsNull (line 32) | Optional<InvitationEntity> findByAccountAndEmailAndAcceptedAtIsNull(Ac...
FILE: server/src/main/java/tech/amak/portbuddy/server/db/repo/PasswordResetTokenRepository.java
type PasswordResetTokenRepository (line 25) | public interface PasswordResetTokenRepository extends JpaRepository<Pass...
method findByToken (line 26) | Optional<PasswordResetTokenEntity> findByToken(String token);
method deleteByUser (line 28) | void deleteByUser(UserEntity user);
FILE: server/src/main/java/tech/amak/portbuddy/server/db/repo/PortReservationRepository.java
type PortReservationRepository (line 28) | public interface PortReservationRepository extends JpaRepository<PortRes...
method findAllByAccount (line 30) | List<PortReservationEntity> findAllByAccount(AccountEntity account);
method findByIdAndAccount (line 32) | Optional<PortReservationEntity> findByIdAndAccount(UUID id, AccountEnt...
method existsByPublicHostAndPublicPort (line 34) | boolean existsByPublicHostAndPublicPort(String publicHost, Integer pub...
method countByPublicHost (line 36) | long countByPublicHost(String publicHost);
method findMaxPortByHost (line 38) | @Query("select max(pr.publicPort) from PortReservationEntity pr where ...
method findByAccountAndPublicHostAndPublicPort (line 41) | Optional<PortReservationEntity> findByAccountAndPublicHostAndPublicPor...
method findAllByAccountAndPublicPort (line 45) | List<PortReservationEntity> findAllByAccountAndPublicPort(AccountEntit...
method existsByAccountAndName (line 47) | boolean existsByAccountAndName(AccountEntity account, String name);
method findByAccountAndNameIgnoreCase (line 49) | Optional<PortReservationEntity> findByAccountAndNameIgnoreCase(Account...
method findMinimalFreePort (line 55) | @Query(value = """
FILE: server/src/main/java/tech/amak/portbuddy/server/db/repo/StripeEventRepository.java
type StripeEventRepository (line 21) | public interface StripeEventRepository extends JpaRepository<StripeEvent...
FILE: server/src/main/java/tech/amak/portbuddy/server/db/repo/TunnelRepository.java
type TunnelRepository (line 36) | @Repository
method existsByDomainAndStatus (line 39) | boolean existsByDomainAndStatus(DomainEntity domain, TunnelStatus stat...
method existsByDomainAndStatusNot (line 41) | boolean existsByDomainAndStatusNot(DomainEntity domain, TunnelStatus s...
method findFirstByAccountIdAndLocalHostAndLocalPortAndDomainIsNotNullOrderByCreatedAtDesc (line 43) | Optional<TunnelEntity> findFirstByAccountIdAndLocalHostAndLocalPortAnd...
method findUsedTunnel (line 46) | default Optional<TunnelEntity> findUsedTunnel(final UUID accountId,
method findAllByAccountId (line 53) | Page<TunnelEntity> findAllByAccountId(UUID accountId, Pageable pageable);
method existsByPortReservationAndStatusNot (line 55) | boolean existsByPortReservationAndStatusNot(PortReservationEntity port...
method findFirstByAccountIdAndLocalHostAndLocalPortAndPortReservationIsNotNullOrderByCreatedAtDesc (line 57) | Optional<TunnelEntity> findFirstByAccountIdAndLocalHostAndLocalPortAnd...
method pageByAccountOrderByLastHeartbeatDescNullsLast (line 60) | @Query(value = """
method countByAccountIdAndStatusIn (line 70) | long countByAccountIdAndStatusIn(UUID accountId, List<TunnelStatus> st...
method findByAccountIdAndStatusInOrderByLastHeartbeatAtAscCreatedAtAsc (line 72) | List<TunnelEntity> findByAccountIdAndStatusInOrderByLastHeartbeatAtAsc...
method countByStatusIn (line 75) | long countByStatusIn(List<TunnelStatus> statuses);
method closeStaleConnected (line 84) | @Modifying(clearAutomatically = true, flushAutomatically = true)
method findAdminActiveTunnels (line 94) | @Query(value = """
FILE: server/src/main/java/tech/amak/portbuddy/server/db/repo/UserAccountRepository.java
type UserAccountRepository (line 27) | public interface UserAccountRepository extends JpaRepository<UserAccount...
method findAllByUserId (line 29) | List<UserAccountEntity> findAllByUserId(UUID userId);
method findLatestUsedByUserId (line 31) | @Query("""
method findByUserIdAndAccountId (line 40) | @Query("""
FILE: server/src/main/java/tech/amak/portbuddy/server/db/repo/UserRepository.java
type UserRepository (line 30) | public interface UserRepository extends JpaRepository<UserEntity, UUID> {
method findById (line 32) | @EntityGraph(attributePaths = "accounts")
method findByAuthProviderAndExternalId (line 36) | Optional<UserEntity> findByAuthProviderAndExternalId(String authProvid...
method findByEmailIgnoreCase (line 38) | Optional<UserEntity> findByEmailIgnoreCase(String email);
method findAllByAccount (line 40) | @Query("SELECT ua.user FROM UserAccountEntity ua WHERE ua.account = :a...
method findAdminUsers (line 43) | @Query(value = """
FILE: server/src/main/java/tech/amak/portbuddy/server/mail/EmailService.java
class EmailService (line 37) | @Service
method sendTemplate (line 54) | @Async
FILE: server/src/main/java/tech/amak/portbuddy/server/mail/WelcomeEmailService.java
class WelcomeEmailService (line 30) | @Service
method onUserCreated (line 43) | @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
method buildFullName (line 78) | private static String buildFullName(final String firstName, final Stri...
FILE: server/src/main/java/tech/amak/portbuddy/server/security/ApiTokenAuthFilter.java
class ApiTokenAuthFilter (line 49) | @Component
method doFilterInternal (line 56) | @Override
FILE: server/src/main/java/tech/amak/portbuddy/server/security/JwtConfig.java
class JwtConfig (line 36) | @Configuration
method jwkSource (line 43) | @Bean
method jwtEncoder (line 48) | @Bean
method jwtDecoder (line 53) | @Bean
FILE: server/src/main/java/tech/amak/portbuddy/server/security/JwtService.java
class JwtService (line 35) | @Service
method createToken (line 54) | public String createToken(final Map<String, Object> claims, final Stri...
method createToken (line 68) | public String createToken(final Map<String, Object> claims, final Stri...
method resolveUserId (line 91) | public static UUID resolveUserId(final Jwt jwt) {
method resolveAccountId (line 101) | public static UUID resolveAccountId(final Jwt jwt) {
FILE: server/src/main/java/tech/amak/portbuddy/server/security/Oauth2SuccessHandler.java
class Oauth2SuccessHandler (line 41) | @Component
method Oauth2SuccessHandler (line 75) | public Oauth2SuccessHandler(final JwtService jwtService,
method onAuthenticationSuccess (line 87) | @Override
method resolveProvider (line 162) | private static String resolveProvider(final Authentication authenticat...
method fetchGithubEmail (line 169) | private String fetchGithubEmail(final Authentication authentication) {
method asNullableString (line 208) | private static String asNullableString(final Map<String, Object> attrs...
method provisionOrRedirectOnMissingEmail (line 213) | private UserProvisioningService.ProvisionedUser provisionOrRedirectOnM...
FILE: server/src/main/java/tech/amak/portbuddy/server/security/RsaKeyProvider.java
class RsaKeyProvider (line 44) | @Component
method RsaKeyProvider (line 67) | public RsaKeyProvider(final AppProperties properties) {
method jwkSource (line 105) | public JWKSource<SecurityContext> jwkSource() {
method parsePublicKey (line 109) | private static RSAPublicKey parsePublicKey(final String pem) throws Ex...
method parsePrivateKey (line 116) | private static RSAPrivateKey parsePrivateKey(final String pem) throws ...
method stripPemHeaders (line 123) | private static String stripPemHeaders(final String pem, final String t...
FILE: server/src/main/java/tech/amak/portbuddy/server/security/SecurityConfig.java
class SecurityConfig (line 39) | @Configuration
method apiSecurityFilterChain (line 48) | @Bean
method webSecurityFilterChain (line 82) | @Bean
method passwordEncoder (line 103) | @Bean
method jwtAuthenticationConverter (line 108) | @Bean
FILE: server/src/main/java/tech/amak/portbuddy/server/security/ThreatBlockedException.java
class ThreatBlockedException (line 17) | public class ThreatBlockedException extends SecurityException {
method ThreatBlockedException (line 19) | public ThreatBlockedException(final String message) {
FILE: server/src/main/java/tech/amak/portbuddy/server/service/ApiTokenService.java
class ApiTokenService (line 37) | @Service
method ApiTokenService (line 44) | public ApiTokenService(final ApiKeyRepository apiKeyRepository) {
method createToken (line 56) | @Transactional
method listTokens (line 77) | @Transactional(readOnly = true)
method revoke (line 92) | @Transactional
method validateAndGetUserId (line 112) | @Transactional
method validateAndGetApiKey (line 131) | @Transactional
method generateRawToken (line 150) | private String generateRawToken() {
method sha256 (line 156) | private String sha256(final String val) {
method toInstant (line 175) | private static Instant toInstant(final OffsetDateTime odt) {
FILE: server/src/main/java/tech/amak/portbuddy/server/service/DomainService.java
class DomainService (line 45) | @Service
method getDomains (line 61) | @Transactional(readOnly = true)
method createDomain (line 76) | @Transactional
method assignRandomDomain (line 100) | @Transactional
method updateDomain (line 118) | @Transactional
method deleteDomain (line 148) | @Transactional
method setPasscode (line 170) | @Transactional
method clearPasscode (line 193) | @Transactional
method updateCustomDomain (line 209) | @Transactional
method deleteCustomDomain (line 230) | @Transactional
method verifyCname (line 249) | @Transactional
method checkCname (line 283) | private boolean checkCname(final String domain, final String expectedT...
method markSslActive (line 317) | @Transactional
method resolveDomain (line 343) | @Transactional(readOnly = true)
method isTunnelConnected (line 391) | private boolean isTunnelConnected(final DomainEntity domain) {
method isTunnelActive (line 395) | private boolean isTunnelActive(final DomainEntity domain) {
method generateRandomSubdomain (line 399) | private String generateRandomSubdomain() {
FILE: server/src/main/java/tech/amak/portbuddy/server/service/PaymentCleanupService.java
class PaymentCleanupService (line 33) | @Service
method cleanupFailedPayments (line 45) | @Scheduled(
FILE: server/src/main/java/tech/amak/portbuddy/server/service/PortReservationService.java
class PortReservationService (line 36) | @Service
method getReservations (line 48) | @Transactional(readOnly = true)
method createReservation (line 61) | @Transactional
method computeNextPort (line 115) | private Integer computeNextPort(final String host, final int min, fina...
method deleteReservation (line 126) | @Transactional
method resolveForNetExpose (line 145) | @Transactional
method isReservationInUse (line 215) | private boolean isReservationInUse(final PortReservationEntity reserva...
method updateReservation (line 222) | @Transactional
FILE: server/src/main/java/tech/amak/portbuddy/server/service/ProxyDiscoveryService.java
class ProxyDiscoveryService (line 28) | @Service
method listPublicHosts (line 45) | public List<String> listPublicHosts() {
method firstNonBlank (line 69) | private String firstNonBlank(final String... values) {
FILE: server/src/main/java/tech/amak/portbuddy/server/service/StaleTunnelsReaper.java
class StaleTunnelsReaper (line 33) | @Service
method closeStaleTunnels (line 45) | @Scheduled(
FILE: server/src/main/java/tech/amak/portbuddy/server/service/StripeService.java
class StripeService (line 37) | @Slf4j
method StripeService (line 43) | public StripeService(final AppProperties properties) {
method createCheckoutSession (line 57) | public String createCheckoutSession(final AccountEntity account, final...
method createCheckoutSession (line 113) | public String createCheckoutSession(final AccountEntity account, final...
method cancelSubscription (line 123) | public void cancelSubscription(final String subscriptionId) throws Str...
method cancelSubscription (line 138) | public void cancelSubscription(final AccountEntity account) throws Str...
method createPortalSession (line 149) | public String createPortalSession(final AccountEntity account) throws ...
method updateExtraTunnels (line 169) | public void updateExtraTunnels(final AccountEntity account, final int ...
method getOrCreateCustomer (line 221) | private String getOrCreateCustomer(final AccountEntity account) throws...
FILE: server/src/main/java/tech/amak/portbuddy/server/service/StripeWebhookService.java
class StripeWebhookService (line 26) | @Service
method constructEvent (line 37) | public Event constructEvent(final String payload, final String sigHead...
FILE: server/src/main/java/tech/amak/portbuddy/server/service/TeamService.java
class TeamService (line 44) | @Service
method getMembers (line 61) | public List<UserEntity> getMembers(final AccountEntity account) {
method getPendingInvitations (line 71) | public List<InvitationEntity> getPendingInvitations(final AccountEntit...
method inviteMember (line 83) | @Transactional
method removeMember (line 123) | @Transactional
method cancelInvitation (line 141) | @Transactional
method acceptInvitation (line 159) | @Transactional
method switchAccount (line 193) | @Transactional
method resendInvitation (line 210) | @Transactional
method sendInvitationEmail (line 230) | private void sendInvitationEmail(final InvitationEntity invitation) {
FILE: server/src/main/java/tech/amak/portbuddy/server/service/TunnelService.java
class TunnelService (line 42) | @Service
method createHttpTunnel (line 68) | @Transactional
method createNetTunnel (line 89) | @Transactional
method checkSubscriptionStatus (line 98) | private void checkSubscriptionStatus(final AccountEntity account) {
method checkTunnelLimit (line 117) | private void checkTunnelLimit(final AccountEntity account) {
method calculateTunnelLimit (line 136) | public int calculateTunnelLimit(final AccountEntity account) {
method enforceTunnelLimit (line 150) | @Transactional
method closeAllTunnels (line 161) | @Transactional
method closeExcessTunnels (line 166) | private void closeExcessTunnels(final AccountEntity account, final int...
method createTunnel (line 195) | private TunnelEntity createTunnel(final UUID accountId,
method updateTunnelPublicConnection (line 242) | @Transactional
method assignReservation (line 256) | @Transactional
method findByTunnelId (line 273) | public Optional<TunnelEntity> findByTunnelId(final UUID tunnelId) {
method setTempPasscodeHash (line 281) | @Transactional
method getTempPasscodeHash (line 292) | public Optional<String> getTempPasscodeHash(final UUID tunnelId) {
method markConnected (line 305) | @Transactional
method heartbeat (line 325) | @Transactional
method markClosed (line 344) | @Transactional
FILE: server/src/main/java/tech/amak/portbuddy/server/service/threatfox/ThreatFoxClient.java
type ThreatFoxClient (line 27) | @FeignClient(
method fetchIoc (line 35) | @PostMapping("/api/v1/")
class Configuration (line 38) | @RequiredArgsConstructor
method authorizationHeaderForwarder (line 44) | @Bean
FILE: server/src/main/java/tech/amak/portbuddy/server/service/threatfox/ThreatFoxService.java
class ThreatFoxService (line 30) | @Slf4j
method fetchData (line 59) | @Scheduled(
method process (line 72) | private void process(final ThreatFoxResponse response) {
method isRelevantType (line 87) | private boolean isRelevantType(final String type) {
method normalize (line 91) | private String normalize(final ThreatFoxIoc ioc) {
method normalize (line 98) | private String normalize(final String ioc) {
method extractDomain (line 105) | private String extractDomain(final String url) {
method isBlacklisted (line 112) | private boolean isBlacklisted(final String target) {
method checkThreat (line 129) | public void checkThreat(final String host, final int port) {
FILE: server/src/main/java/tech/amak/portbuddy/server/service/user/MissingEmailException.java
class MissingEmailException (line 20) | public class MissingEmailException extends RuntimeException {
method MissingEmailException (line 22) | public MissingEmailException(final String message) {
FILE: server/src/main/java/tech/amak/portbuddy/server/service/user/PasswordResetService.java
class PasswordResetService (line 35) | @Service
method requestReset (line 51) | @Transactional
method generateResetPasswordLink (line 85) | @Transactional
method validateToken (line 109) | @Transactional(readOnly = true)
method resetPassword (line 125) | @Transactional
FILE: server/src/main/java/tech/amak/portbuddy/server/service/user/UserProvisioningService.java
class UserProvisioningService (line 44) | @Service
method createLocalUser (line 64) | @Transactional
method provision (line 144) | @Transactional
method defaultAccountName (line 269) | private static String defaultAccountName(final String firstName, final...
method normalizeEmail (line 282) | private static String normalizeEmail(final String email) {
method determineRoles (line 290) | private Set<Role> determineRoles(final boolean isAccountOwner) {
FILE: server/src/main/java/tech/amak/portbuddy/server/tunnel/PermissiveSubprotocolHandshakeHandler.java
class PermissiveSubprotocolHandshakeHandler (line 28) | public class PermissiveSubprotocolHandshakeHandler extends DefaultHandsh...
method selectProtocol (line 30) | @Override
FILE: server/src/main/java/tech/amak/portbuddy/server/tunnel/PublicWebSocketProxyHandler.java
class PublicWebSocketProxyHandler (line 43) | @Slf4j
method afterConnectionEstablished (line 53) | @Override
method handleTextMessage (line 111) | @Override
method handleBinaryMessage (line 124) | @Override
method afterConnectionClosed (line 137) | @Override
method extractSubdomain (line 151) | private String extractSubdomain(final WebSocketSession session) {
method firstNonBlank (line 214) | private static String firstNonBlank(final String a, final String b) {
method normalizePublicPath (line 229) | private static String normalizePublicPath(final String subdomain, fina...
method collectForwardedHandshakeHeaders (line 256) | private Map<String, String> collectForwardedHandshakeHeaders(final Web...
FILE: server/src/main/java/tech/amak/portbuddy/server/tunnel/TunnelRegistry.java
class TunnelRegistry (line 44) | @Slf4j
method register (line 63) | public boolean register(final TunnelEntity tunnelEntity, final WebSock...
method register (line 80) | private Tunnel register(final String subdomain, final UUID tunnelId, f...
method getBySubdomain (line 88) | public Tunnel getBySubdomain(final String subdomain) {
method getByTunnelId (line 92) | public Tunnel getByTunnelId(final UUID tunnelId) {
method forwardRequest (line 106) | public CompletableFuture<HttpTunnelMessage> forwardRequest(final Strin...
method onResponse (line 150) | public void onResponse(final UUID tunnelId, final HttpTunnelMessage re...
method sendWsToClient (line 170) | public void sendWsToClient(final UUID tunnelId, final WsTunnelMessage ...
method registerBrowserWs (line 192) | public void registerBrowserWs(final UUID tunnelId,
method unregisterBrowserWs (line 213) | public Ids unregisterBrowserWs(final WebSocketSession browserSession) {
method findIdsByBrowserSession (line 232) | public Ids findIdsByBrowserSession(final WebSocketSession browserSessi...
method getBrowserSession (line 252) | public WebSocketSession getBrowserSession(final UUID tunnelId, final S...
method closeTunnel (line 265) | public void closeTunnel(final UUID tunnelId) {
class Ids (line 306) | @Data
class Tunnel (line 313) | @RequiredArgsConstructor
method subdomain (line 328) | public String subdomain() {
method tunnelId (line 332) | public UUID tunnelId() {
method accountId (line 336) | public UUID accountId() {
method session (line 340) | public WebSocketSession session() {
method pending (line 344) | public Map<String, CompletableFuture<HttpTunnelMessage>> pending() {
method isOpen (line 348) | public boolean isOpen() {
method browserByConnection (line 352) | public Map<String, WebSocketSession> browserByConnection() {
method browserReverse (line 356) | public Map<WebSocketSession, Ids> browserReverse() {
FILE: server/src/main/java/tech/amak/portbuddy/server/tunnel/TunnelWebSocketHandler.java
class TunnelWebSocketHandler (line 38) | @Slf4j
method afterConnectionEstablished (line 47) | @Override
method closeWebsocket (line 64) | private void closeWebsocket(final WebSocketSession session,
method handleTextMessage (line 74) | @Override
method handleWsFromClient (line 111) | private void handleWsFromClient(final UUID tunnelId, final WsTunnelMes...
method afterConnectionClosed (line 136) | @Override
method extractTunnelId (line 147) | private UUID extractTunnelId(final WebSocketSession session) {
FILE: server/src/main/java/tech/amak/portbuddy/server/tunnel/WebSocketConfig.java
class WebSocketConfig (line 27) | @Configuration
method registerWebSocketHandlers (line 36) | @Override
method websocketContainer (line 54) | @Bean
FILE: server/src/main/java/tech/amak/portbuddy/server/web/AuthController.java
class AuthController (line 49) | @RestController
method tokenExchange (line 68) | @PostMapping("/token-exchange")
method isCliVersionSupported (line 100) | private boolean isCliVersionSupported(final String clientVersion, fina...
method compareVersions (line 109) | private int compareVersions(final String v1, final String v2) {
method parseIntSafe (line 123) | private int parseIntSafe(final String part) {
method register (line 134) | @PostMapping("/register")
method login (line 161) | @PostMapping("/login")
method requestPasswordReset (line 203) | @PostMapping("/password-reset/request")
method validateResetToken (line 215) | @GetMapping("/password-reset/validate")
method confirmPasswordReset (line 229) | @PostMapping("/password-reset/confirm")
FILE: server/src/main/java/tech/amak/portbuddy/server/web/DomainsController.java
class DomainsController (line 48) | @RestController
method list (line 62) | @GetMapping
method create (line 77) | @PostMapping
method update (line 85) | @PutMapping("/{id}")
method delete (line 93) | @DeleteMapping("/{id}")
method setPasscode (line 110) | @PutMapping("/{id}/passcode")
method deletePasscode (line 124) | @DeleteMapping("/{id}/passcode")
method updateCustomDomain (line 140) | @PutMapping("/{id}/custom-domain")
method deleteCustomDomain (line 154) | @DeleteMapping("/{id}/custom-domain")
method verifyCname (line 169) | @PostMapping("/{id}/verify-cname")
method getAccount (line 177) | private AccountEntity getAccount(final Jwt jwt) {
method toDto (line 189) | private static DomainDto toDto(final DomainEntity domain) {
FILE: server/src/main/java/tech/amak/portbuddy/server/web/ExposeController.java
class ExposeController (line 45) | @RestController
method exposeHttp (line 69) | @PostMapping("/http")
method exposeNet (line 113) | @PostMapping("/net")
method extractApiKeyId (line 148) | private String extractApiKeyId(final Jwt jwt) {
method validateUser (line 153) | private ValidatedUser validateUser(final Jwt jwt) {
FILE: server/src/main/java/tech/amak/portbuddy/server/web/IngressController.java
class IngressController (line 60) | @Slf4j
method ingressPathBased (line 89) | @RequestMapping("/_/{subdomain:.+}/**")
method ingressCustomDomainPathBased (line 109) | @RequestMapping("/_custom/{customDomain:.+}/**")
method forwardViaTunnel (line 128) | private void forwardViaTunnel(final String subdomain,
method isAuthorized (line 262) | private boolean isAuthorized(final String subdomain,
method matches (line 296) | private boolean matches(final String raw, final String hash) {
method findCookie (line 307) | private Optional<Cookie> findCookie(final HttpServletRequest request, ...
method issueCookie (line 314) | private void issueCookie(final HttpServletResponse response, final Str...
FILE: server/src/main/java/tech/amak/portbuddy/server/web/IngressResolveController.java
class IngressResolveController (line 36) | @RestController
method resolveOwner (line 51) | @GetMapping("/resolve/{subdomain}")
method resolveCustomOwner (line 66) | @GetMapping("/resolve-custom/{domain}")
method isSubscriptionActive (line 79) | private boolean isSubscriptionActive(final TunnelRegistry.Tunnel tunne...
FILE: server/src/main/java/tech/amak/portbuddy/server/web/InternalDomainController.java
class InternalDomainController (line 29) | @RestController
method markSslActive (line 41) | @PostMapping("/ssl-active")
FILE: server/src/main/java/tech/amak/portbuddy/server/web/InternalEmailController.java
class InternalEmailController (line 34) | @RestController
method sendDnsInstructions (line 48) | @PostMapping("/dns-instructions")
FILE: server/src/main/java/tech/amak/portbuddy/server/web/JwksController.java
class JwksController (line 35) | @RestController
method jwks (line 51) | @GetMapping(value = "/.well-known/jwks.json", produces = MediaType.APP...
FILE: server/src/main/java/tech/amak/portbuddy/server/web/PaymentController.java
class PaymentController (line 41) | @Slf4j
method createCheckoutSession (line 60) | @Transactional
method createPortalSession (line 79) | @Transactional
method cancelSubscription (line 95) | @Transactional
class CheckoutRequest (line 110) | @Data
class SessionResponse (line 115) | @Data
FILE: server/src/main/java/tech/amak/portbuddy/server/web/PortsController.java
class PortsController (line 46) | @RestController
method list (line 62) | @GetMapping
method create (line 77) | @PostMapping
method delete (line 97) | @DeleteMapping("/{id}")
method update (line 112) | @PutMapping("/{id}")
method hosts (line 131) | @GetMapping("/hosts")
method hostRange (line 139) | @GetMapping("/hosts/{host}/range")
method getAccount (line 146) | private AccountEntity getAccount(final Jwt jwt) {
method toDto (line 157) | private static PortReservationDto toDto(final PortReservationEntity e) {
FILE: server/src/main/java/tech/amak/portbuddy/server/web/StripeWebhookController.java
class StripeWebhookController (line 49) | @Slf4j
method handleStripeWebhook (line 70) | @Transactional
method handleCheckoutSessionCompleted (line 145) | private void handleCheckoutSessionCompleted(final Event event) {
method sendSubscriptionSuccessEmail (line 198) | private void sendSubscriptionSuccessEmail(final AccountEntity account) {
method handleSubscriptionEvent (line 218) | private void handleSubscriptionEvent(final Event event) {
method handleInvoicePaymentFailed (line 297) | private void handleInvoicePaymentFailed(final Event event) {
FILE: server/src/main/java/tech/amak/portbuddy/server/web/TeamController.java
class TeamController (line 48) | @RestController
method getMembers (line 65) | @GetMapping("/members")
method getInvitations (line 79) | @GetMapping("/invitations")
method inviteMember (line 95) | @PostMapping("/invitations")
method cancelInvitation (line 105) | @DeleteMapping("/invitations/{id}")
method resendInvitation (line 114) | @PostMapping("/invitations/{id}/resend")
method removeMember (line 129) | @DeleteMapping("/members/{userId}")
method acceptInvitation (line 139) | @PostMapping("/accept")
method getAccount (line 147) | private AccountEntity getAccount(final Jwt jwt) {
method getUser (line 153) | private UserEntity getUser(final Jwt jwt) {
method toMemberDto (line 159) | private MemberDto toMemberDto(final UserEntity user, final AccountEnti...
method toInvitationDto (line 173) | private InvitationDto toInvitationDto(final InvitationEntity invitatio...
class MemberDto (line 183) | @Data
class InvitationDto (line 195) | @Data
class InviteRequest (line 205) | @Data
FILE: server/src/main/java/tech/amak/portbuddy/server/web/TokensController.java
class TokensController (line 41) | @RestController
method list (line 56) | @GetMapping
method create (line 69) | @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
method revoke (line 88) | @DeleteMapping("/{id}")
class CreateTokenRequest (line 95) | @Data
FILE: server/src/main/java/tech/amak/portbuddy/server/web/TunnelStatusController.java
class TunnelStatusController (line 34) | @RestController
method connected (line 45) | @PostMapping(path = "/{tunnelId}/connected")
method heartbeat (line 54) | @PostMapping(path = "/{tunnelId}/heartbeat")
method closed (line 62) | @PostMapping(path = "/{tunnelId}/closed")
FILE: server/src/main/java/tech/amak/portbuddy/server/web/TunnelsController.java
class TunnelsController (line 37) | @RestController
method page (line 52) | @GetMapping
method toView (line 61) | private static TunnelView toView(final TunnelEntity tunnel) {
FILE: server/src/main/java/tech/amak/portbuddy/server/web/UsersController.java
class UsersController (line 59) | @RestController
method details (line 79) | @GetMapping("/details")
method updateProfile (line 108) | @PatchMapping(path = "/profile", consumes = MediaType.APPLICATION_JSON...
method updateAccount (line 140) | @PatchMapping(path = "/account", consumes = MediaType.APPLICATION_JSON...
method updateExtraTunnels (line 165) | @PatchMapping(path = "/account/tunnels", consumes = MediaType.APPLICAT...
method getAccounts (line 226) | @Transactional
method switchAccount (line 248) | @Transactional
method toAccountDto (line 276) | private AccountDto toAccountDto(final AccountEntity account) {
method resolveUser (line 291) | private UserEntity resolveUser(final Jwt jwt) {
method resolveUserAccount (line 297) | private UserAccountEntity resolveUserAccount(final Jwt jwt) {
method normalizeNullable (line 304) | private static String normalizeNullable(final String value) {
class UserDetailsResponse (line 312) | @Data
class UserDto (line 318) | @Data
class AccountDto (line 328) | @Data
class UpdateProfileRequest (line 342) | @Data
class UpdateAccountRequest (line 348) | @Data
class UpdateTunnelsRequest (line 353) | @Data
class UserAccountDto (line 358) | @Data
FILE: server/src/main/java/tech/amak/portbuddy/server/web/admin/AdminAccountController.java
class AdminAccountController (line 37) | @RestController
method listAccounts (line 52) | @GetMapping
method blockAccount (line 65) | @PostMapping("/{accountId}/block")
method unblockAccount (line 83) | @PostMapping("/{accountId}/unblock")
FILE: server/src/main/java/tech/amak/portbuddy/server/web/admin/AdminSystemController.java
class AdminSystemController (line 36) | @RestController
method getSystemStats (line 51) | @GetMapping("/stats")
method getDailyStats (line 66) | @GetMapping("/stats/daily")
FILE: server/src/main/java/tech/amak/portbuddy/server/web/admin/AdminTunnelController.java
class AdminTunnelController (line 37) | @RestController
method listActiveTunnels (line 52) | @GetMapping
method closeTunnel (line 64) | @PostMapping("/{tunnelId}/close")
FILE: server/src/main/java/tech/amak/portbuddy/server/web/admin/AdminUserController.java
class AdminUserController (line 30) | @RestController
method listUsers (line 45) | @GetMapping
FILE: server/src/main/java/tech/amak/portbuddy/server/web/advice/GlobalExceptionHandler.java
class GlobalExceptionHandler (line 26) | @Slf4j
method handleResponseStatusException (line 30) | @ExceptionHandler(ResponseStatusException.class)
method handleIllegalArgumentException (line 36) | @ExceptionHandler(IllegalArgumentException.class)
method handleGlobalException (line 42) | @ExceptionHandler(Exception.class)
FILE: server/src/main/resources/db/migration/V10__link_http_tunnels_to_domain.sql
type idx_tunnels_domain_id (line 5) | CREATE INDEX idx_tunnels_domain_id ON tunnels(domain_id)
FILE: server/src/main/resources/db/migration/V13__password_reset_tokens.sql
type password_reset_tokens (line 5) | CREATE TABLE password_reset_tokens (
type idx_password_reset_tokens_token (line 13) | CREATE INDEX idx_password_reset_tokens_token ON password_reset_tokens(to...
FILE: server/src/main/resources/db/migration/V14__port_reservations.sql
type port_reservations (line 1) | CREATE TABLE port_reservations (
type idx_port_reservations_account_id (line 11) | CREATE INDEX idx_port_reservations_account_id ON port_reservations(accou...
type idx_port_reservations_public_host (line 12) | CREATE INDEX idx_port_reservations_public_host ON port_reservations(publ...
FILE: server/src/main/resources/db/migration/V15__add_user_to_port_reservations.sql
type idx_port_reservations_user_id (line 6) | CREATE INDEX IF NOT EXISTS idx_port_reservations_user_id ON port_reserva...
FILE: server/src/main/resources/db/migration/V16__add_port_reservation_to_tunnels.sql
type idx_tunnels_port_reservation_id (line 9) | CREATE INDEX IF NOT EXISTS idx_tunnels_port_reservation_id ON tunnels (p...
FILE: server/src/main/resources/db/migration/V17__soft_delete_port_reservations.sql
type uq_port_reservations_host_port_active (line 21) | CREATE UNIQUE INDEX IF NOT EXISTS uq_port_reservations_host_port_active
FILE: server/src/main/resources/db/migration/V1__accounts_and_users.sql
type accounts (line 2) | CREATE TABLE IF NOT EXISTS accounts (
type users (line 10) | CREATE TABLE IF NOT EXISTS users (
type idx_users_account_id (line 24) | CREATE INDEX IF NOT EXISTS idx_users_account_id ON users(account_id)
type idx_users_email (line 25) | CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email ON users(email)
FILE: server/src/main/resources/db/migration/V20__add_custom_domain_to_domains.sql
type idx_domains_custom_domain (line 7) | CREATE INDEX idx_domains_custom_domain ON domains(custom_domain)
FILE: server/src/main/resources/db/migration/V24__create_stripe_events_table.sql
type stripe_events (line 5) | CREATE TABLE stripe_events (
FILE: server/src/main/resources/db/migration/V25__create_invitations_table.sql
type invitations (line 5) | CREATE TABLE invitations (
type idx_invitations_account_id (line 16) | CREATE INDEX idx_invitations_account_id ON invitations(account_id)
type idx_invitations_token (line 17) | CREATE INDEX idx_invitations_token ON invitations(token)
type idx_invitations_email (line 18) | CREATE INDEX idx_invitations_email ON invitations(email)
FILE: server/src/main/resources/db/migration/V26__many_to_many_users_accounts.sql
type user_accounts (line 6) | CREATE TABLE IF NOT EXISTS user_accounts (
type idx_user_accounts_user_id (line 25) | CREATE INDEX IF NOT EXISTS idx_user_accounts_user_id ON user_accounts(us...
type idx_user_accounts_account_id (line 26) | CREATE INDEX IF NOT EXISTS idx_user_accounts_account_id ON user_accounts...
FILE: server/src/main/resources/db/migration/V2__api_keys.sql
type api_keys (line 2) | CREATE TABLE IF NOT EXISTS api_keys (
type idx_api_keys_user_id (line 13) | CREATE INDEX IF NOT EXISTS idx_api_keys_user_id ON api_keys(user_id)
type idx_api_keys_created_at (line 14) | CREATE INDEX IF NOT EXISTS idx_api_keys_created_at ON api_keys(created_at)
FILE: server/src/main/resources/db/migration/V30__unique_port_reservation_name.sql
type idx_port_reservations_account_id_name_unique (line 18) | CREATE UNIQUE INDEX idx_port_reservations_account_id_name_unique
FILE: server/src/main/resources/db/migration/V3__tunnels.sql
type tunnels (line 2) | CREATE TABLE IF NOT EXISTS tunnels (
type idx_tunnels_user_id (line 24) | CREATE INDEX IF NOT EXISTS idx_tunnels_user_id ON tunnels(user_id)
type idx_tunnels_api_key_id (line 25) | CREATE INDEX IF NOT EXISTS idx_tunnels_api_key_id ON tunnels(api_key_id)
type idx_tunnels_status (line 26) | CREATE INDEX IF NOT EXISTS idx_tunnels_status ON tunnels(status)
type idx_tunnels_created_at (line 27) | CREATE INDEX IF NOT EXISTS idx_tunnels_created_at ON tunnels(created_at)
FILE: server/src/main/resources/db/migration/V4__shedlock_and_heartbeat_indexes.sql
type shedlock (line 2) | CREATE TABLE shedlock
type idx_tunnels_status_last_heartbeat (line 12) | CREATE INDEX IF NOT EXISTS idx_tunnels_status_last_heartbeat ON tunnels ...
FILE: server/src/main/resources/db/migration/V6__create_domains_table.sql
type domains (line 1) | CREATE TABLE domains (
type idx_domains_account_id (line 10) | CREATE INDEX idx_domains_account_id ON domains(account_id)
FILE: server/src/main/resources/db/migration/V7__link_tunnels_to_account.sql
type idx_tunnels_account_id (line 12) | CREATE INDEX idx_tunnels_account_id ON tunnels(account_id)
FILE: server/src/main/resources/db/migration/V8__add_user_id_to_tunnels.sql
type idx_tunnels_user_id (line 10) | CREATE INDEX idx_tunnels_user_id ON tunnels(user_id)
FILE: server/src/main/resources/db/migration/V9__link_api_keys_to_account.sql
type idx_api_keys_account_id (line 12) | CREATE INDEX idx_api_keys_account_id ON api_keys(account_id)
FILE: server/src/test/java/tech/amak/portbuddy/server/security/Oauth2SuccessHandlerTest.java
class Oauth2SuccessHandlerTest (line 51) | @ExtendWith(MockitoExtension.class)
method setUp (line 75) | @BeforeEach
method onAuthenticationSuccess_WithEmail_ShouldProvisionAndRedirect (line 91) | @Test
method onAuthenticationSuccess_MissingEmailGithub_ShouldFetchEmail (line 113) | @Test
method onAuthenticationSuccess_MissingEmailNotGithub_ShouldRedirectWithError (line 157) | @Test
FILE: server/src/test/java/tech/amak/portbuddy/server/service/DomainServiceTest.java
class DomainServiceTest (line 49) | @ExtendWith(MockitoExtension.class)
method setUp (line 68) | @BeforeEach
method createDomain_Success (line 101) | @Test
method createDomain_RetryOnCollision (line 115) | @Test
method updateDomain_Success (line 130) | @Test
method updateDomain_ActiveTunnel (line 149) | @Test
method deleteDomain_Success (line 162) | @Test
method deleteCustomDomain_Success (line 177) | @Test
FILE: server/src/test/java/tech/amak/portbuddy/server/service/PaymentCleanupServiceTest.java
class PaymentCleanupServiceTest (line 37) | @ExtendWith(MockitoExtension.class)
method setUp (line 51) | @BeforeEach
method cleanupFailedPayments_FindsAccounts_FreezesTunnels (line 57) | @Test
method cleanupFailedPayments_NoAccounts_DoesNothing (line 74) | @Test
FILE: server/src/test/java/tech/amak/portbuddy/server/service/PortReservationServiceTest.java
class PortReservationServiceTest (line 39) | @ExtendWith(MockitoExtension.class)
method setUp (line 55) | @BeforeEach
method resolveForNetExpose_ExplicitHostPort_Success (line 64) | @Test
method resolveForNetExpose_ExplicitName_Success (line 83) | @Test
method resolveForNetExpose_ExplicitPortOnly_MultipleFound_TakesFirst (line 99) | @Test
method resolveForNetExpose_ExplicitPortOnly_NotFound_ThrowsException (line 122) | @Test
method updateReservation_DuplicateName_ThrowsException (line 134) | @Test
method updateReservation_SameName_Success (line 148) | @Test
FILE: server/src/test/java/tech/amak/portbuddy/server/service/StaleTunnelsReaperTest.java
class StaleTunnelsReaperTest (line 47) | class StaleTunnelsReaperTest {
method shouldCloseTunnelsInRegistryWhenReaped (line 49) | @Test
FILE: server/src/test/java/tech/amak/portbuddy/server/service/TunnelServiceTest.java
class TunnelServiceTest (line 51) | @ExtendWith(MockitoExtension.class)
method setUp (line 66) | @BeforeEach
method checkTunnelLimit_ActiveSubscription_Success (line 85) | @Test
method checkTunnelLimit_InactiveSubscription_ThrowsException (line 92) | @Test
method markConnected_InactiveSubscription_ThrowsException (line 99) | @Test
method heartbeat_InactiveSubscription_ThrowsException (line 114) | @Test
method checkTunnelLimit_LimitReached_ThrowsException (line 129) | @Test
method enforceTunnelLimit_ExceedsLimit_ClosesExcessTunnels (line 141) | @Test
method checkTunnelLimit_ProPlanNoSubscription_Success (line 167) | @Test
method checkTunnelLimit_ProPlanWithExtraNoSubscription_ThrowsException (line 178) | @Test
method checkTunnelLimit_TeamPlanNoSubscription_ThrowsException (line 188) | @Test
method createRequest (line 197) | private ExposeRequest createRequest() {
FILE: server/src/test/java/tech/amak/portbuddy/server/tunnel/TunnelRegistryLeakTest.java
class TunnelRegistryLeakTest (line 31) | class TunnelRegistryLeakTest {
method shouldCloseBrowserSessionsWhenTunnelIsClosed (line 33) | @Test
method shouldCleanupPendingRequestsOnTimeout (line 72) | @Test
method shouldCleanupBrowserReverseMapWhenUnregistered (line 108) | @Test
FILE: server/src/test/java/tech/amak/portbuddy/server/user/PasswordResetServiceTest.java
class PasswordResetServiceTest (line 46) | @ExtendWith(MockitoExtension.class)
method requestReset_shouldGenerateTokenAndSendEmail_whenUserExists (line 58) | @Test
method requestReset_shouldDoNothing_whenUserDoesNotExist (line 76) | @Test
method validateToken_shouldReturnTrue_whenTokenIsValid (line 87) | @Test
method validateToken_shouldReturnFalse_whenTokenIsExpired (line 99) | @Test
method resetPassword_shouldUpdatePassword_whenTokenIsValid (line 111) | @Test
method resetPassword_shouldThrow_whenTokenIsExpired (line 135) | @Test
FILE: server/src/test/java/tech/amak/portbuddy/server/web/AuthControllerTest.java
class AuthControllerTest (line 57) | @WebMvcTest(AuthController.class)
method register_shouldReturnApiKey (line 94) | @Test
method register_shouldReturnError_whenMissingFields (line 115) | @Test
method requestPasswordReset_shouldReturnNoContent (line 128) | @Test
method tokenExchange_shouldReturnJwt (line 138) | @Test
method login_shouldReturnJwt (line 165) | @Test
method confirmPasswordReset_shouldReturnNoContent (line 196) | @Test
FILE: server/src/test/java/tech/amak/portbuddy/server/web/IngressControllerTest.java
class IngressControllerTest (line 49) | @WebMvcTest(IngressController.class)
method setUp (line 77) | @BeforeEach
method forwardViaTunnel_shouldRejectLargeRequest (line 86) | @Test
method forwardViaTunnel_shouldAcceptSmallRequest (line 112) | @Test
FILE: server/src/test/java/tech/amak/portbuddy/server/web/PaymentControllerTest.java
class PaymentControllerTest (line 54) | @WebMvcTest(PaymentController.class)
method setUp (line 88) | @BeforeEach
method createCheckoutSession_WithAccountAdmin_ShouldSucceed (line 122) | @Test
method createPortalSession_WithAdmin_ShouldSucceed (line 136) | @Test
method cancelSubscription_shouldUpdateAccount (line 145) | @Test
method createJwt (line 169) | private Jwt createJwt(final List<String> roles) {
FILE: server/src/test/java/tech/amak/portbuddy/server/web/StripeWebhookControllerTest.java
class StripeWebhookControllerTest (line 61) | @WebMvcTest(StripeWebhookController.class)
method setUp (line 95) | @BeforeEach
method handleInvoicePaymentFailed_shouldSendEmail (line 105) | @Test
method handleSubscriptionDeleted_shouldSendEmail (line 152) | @Test
method handleCheckoutSessionCompleted_withOldSubscription_shouldCancelOldSubscription (line 203) | @Test
method handleSubscriptionDeleted_forOldSubscription_shouldNotCancelAccount (line 253) | @Test
FILE: server/src/test/java/tech/amak/portbuddy/server/web/TeamControllerTest.java
class TeamControllerTest (line 65) | @WebMvcTest(TeamController.class)
method setUp (line 103) | @BeforeEach
method getMembers_ShouldReturnList (line 142) | @Test
method getInvitations_ShouldReturnList (line 155) | @Test
method inviteMember_ShouldCreateInvitation (line 172) | @Test
method cancelInvitation_ShouldCallService (line 194) | @Test
method resendInvitation_ShouldCallService (line 205) | @Test
method removeMember_ShouldCallService (line 216) | @Test
method removeMember_ShouldReturnBadRequest_WhenRemovingSelf (line 227) | @Test
method getMembers_ShouldThrowException_WhenAccountIdClaimIsMissing (line 237) | @Test
method removeMember_ShouldCallService_WhenAdmin (line 249) | @Test
method createJwt (line 266) | private org.springframework.security.oauth2.jwt.Jwt createJwt() {
FILE: server/src/test/java/tech/amak/portbuddy/server/web/TokensControllerTest.java
class TokensControllerTest (line 34) | @WebMvcTest(TokensController.class)
method revoke_shouldReturnNoContent (line 47) | @Test
FILE: server/src/test/java/tech/amak/portbuddy/server/web/UsersControllerTest.java
class UsersControllerTest (line 64) | @WebMvcTest(UsersController.class)
method setUp (line 114) | @BeforeEach
method updateExtraTunnels_withoutSubscription_shouldReturnCheckoutUrl (line 164) | @Test
method updateExtraTunnels_withSubscription_shouldUpdateStripe (line 183) | @Test
method updateExtraTunnels_proPlan_zeroExtra_withSubscription_shouldCancelSubscription (line 202) | @Test
method createJwt (line 223) | private Jwt createJwt(final List<String> roles) {
FILE: server/src/test/java/tech/amak/portbuddy/server/web/admin/AdminAccountControllerTest.java
class AdminAccountControllerTest (line 38) | @ExtendWith(MockitoExtension.class)
method setUp (line 52) | @BeforeEach
method blockAccount_shouldSetBlockedTrueAndSaveAndCloseTunnels (line 57) | @Test
FILE: ssl-service/src/main/java/tech/amak/portbuddy/sslservice/SslServiceApplication.java
class SslServiceApplication (line 22) | @SpringBootApplication
method main (line 32) | public static void main(final String[] args) {
FILE: ssl-service/src/main/java/tech/amak/portbuddy/sslservice/client/ServerClient.java
type ServerClient (line 24) | @FeignClient(name = "port-buddy-server")
method sendDnsInstructions (line 27) | @PostMapping("/api/internal/email/dns-instructions")
method markSslActive (line 30) | @PostMapping("/api/internal/domains/ssl-active")
FILE: ssl-service/src/main/java/tech/amak/portbuddy/sslservice/config/AsyncConfig.java
class AsyncConfig (line 20) | @Configuration
FILE: ssl-service/src/main/java/tech/amak/portbuddy/sslservice/config/JpaAuditingConfig.java
class JpaAuditingConfig (line 29) | @Configuration
method auditorAware (line 33) | @Bean
method dateTimeProvider (line 41) | @Bean
FILE: ssl-service/src/main/java/tech/amak/portbuddy/sslservice/config/RestConfig.java
class RestConfig (line 22) | @Configuration
method loadBalancedRestTemplate (line 30) | @Bean
FILE: ssl-service/src/main/java/tech/amak/portbuddy/sslservice/config/SchedulingConfig.java
class SchedulingConfig (line 31) | @Configuration
method lockProvider (line 42) | @Bean
FILE: ssl-service/src/main/java/tech/amak/portbuddy/sslservice/domain/CertificateEntity.java
class CertificateEntity (line 39) | @Data
FILE: ssl-service/src/main/java/tech/amak/portbuddy/sslservice/domain/CertificateJobEntity.java
class CertificateJobEntity (line 37) | @Getter
FILE: ssl-service/src/main/java/tech/amak/portbuddy/sslservice/domain/CertificateJobStatus.java
type CertificateJobStatus (line 20) | public enum CertificateJobStatus {
FILE: ssl-service/src/main/java/tech/amak/portbuddy/sslservice/domain/CertificateStatus.java
type CertificateStatus (line 20) | public enum CertificateStatus {
FILE: ssl-service/src/main/java/tech/amak/portbuddy/sslservice/repo/CertificateJobRepository.java
type CertificateJobRepository (line 25) | public interface CertificateJobRepository extends JpaRepository<Certific...
method existsByDomainIgnoreCaseAndStatusIn (line 27) | boolean existsByDomainIgnoreCaseAndStatusIn(String domain, Collection<...
FILE: ssl-service/src/main/java/tech/amak/portbuddy/sslservice/repo/CertificateRepository.java
type CertificateRepository (line 26) | public interface CertificateRepository extends JpaRepository<Certificate...
method findByDomain (line 34) | Optional<CertificateEntity> findByDomain(String domain);
method findByDomainIgnoreCase (line 42) | Optional<CertificateEntity> findByDomainIgnoreCase(String domain);
method findAllByManagedTrue (line 49) | List<CertificateEntity> findAllByManagedTrue();
method findAllByManagedTrueAndExpiresAtBefore (line 57) | List<CertificateEntity> findAllByManagedTrueAndExpiresAtBefore(OffsetD...
FILE: ssl-service/src/main/java/tech/amak/portbuddy/sslservice/security/SecurityConfig.java
class SecurityConfig (line 36) | @Configuration
method securityFilterChain (line 51) | @Bean
method jwtDecoder (line 80) | @Bean
method jwtAuthenticationConverter (line 98) | @Bean
FILE: ssl-service/src/main/java/tech/amak/portbuddy/sslservice/service/AcmeAccountService.java
class AcmeAccountService (line 35) | @Service
method loadAccountKeyPair (line 54) | public KeyPair loadAccountKeyPair() {
FILE: ssl-service/src/main/java/tech/amak/portbuddy/sslservice/service/AcmeCertificateService.java
class AcmeCertificateService (line 68) | @Service
method submitJob (line 94) | @Transactional
method processJobAsync (line 138) | @Async
method performAcmeHttp01Issuance (line 172) | private void performAcmeHttp01Issuance(final CertificateJobEntity job)...
method performAcmeDns01Initiate (line 296) | private void performAcmeDns01Initiate(final CertificateJobEntity job) ...
method confirmDnsAndContinue (line 369) | @Transactional
method pollAuthorizationValidWithRetry (line 503) | private void pollAuthorizationValidWithRetry(final Authorization auth,...
method pollOrderValidWithRetry (line 528) | private void pollOrderValidWithRetry(final Order order, final int maxS...
method buildCsrDer (line 552) | private byte[] buildCsrDer(final List<String> domains, final KeyPair k...
method buildCsrDer (line 574) | private byte[] buildCsrDer(final String domain, final KeyPair keyPair)
method toPem (line 579) | private String toPem(final X509Certificate cert) {
method updateJobMessage (line 589) | private void updateJobMessage(final CertificateJobEntity job, final St...
FILE: ssl-service/src/main/java/tech/amak/portbuddy/sslservice/service/AcmeClientService.java
class AcmeClientService (line 34) | @Service
method newSession (line 45) | public Session newSession() {
method loginOrRegister (line 57) | public Account loginOrRegister(final Session session, final KeyPair ke...
method bindOrder (line 87) | public Order bindOrder(final Session session,
FILE: ssl-service/src/main/java/tech/amak/portbuddy/sslservice/service/CertificateStorageService.java
class CertificateStorageService (line 36) | @Service
method generateRsaKeyPair (line 47) | public KeyPair generateRsaKeyPair() {
method writePrivateKeyPem (line 64) | public Path writePrivateKeyPem(final String domain, final KeyPair keyP...
method writeChainPem (line 78) | public Path writeChainPem(final String domain, final String chainPem) {
method writeCertPem (line 92) | public Path writeCertPem(final String domain, final String certPem) {
method writeFullChainPem (line 106) | public Path writeFullChainPem(final String domain, final String fullCh...
method resolveBaseDir (line 113) | private Path resolveBaseDir() {
method writePem (line 130) | private void writePem(final Path file, final Object pemObject) {
method writeString (line 142) | private void writeString(final Path file, final String content) {
method safe (line 150) | private String safe(final String domain) {
FILE: ssl-service/src/main/java/tech/amak/portbuddy/sslservice/service/DnsResolverService.java
type DnsResolverService (line 20) | public interface DnsResolverService {
method isTxtRecordVisible (line 30) | boolean isTxtRecordVisible(String fqdn, String expectedValue);
FILE: ssl-service/src/main/java/tech/amak/portbuddy/sslservice/service/EmailService.java
type EmailService (line 26) | public interface EmailService {
method sendDnsInstructions (line 35) | void sendDnsInstructions(CertificateJobEntity job, List<Map<String, St...
FILE: ssl-service/src/main/java/tech/amak/portbuddy/sslservice/service/RenewalScheduler.java
class RenewalScheduler (line 35) | @Component
method scheduleRenewals (line 57) | @Scheduled(initialDelay = 5_000, fixedDelay = 300_000)
FILE: ssl-service/src/main/java/tech/amak/portbuddy/sslservice/service/RetryExecutor.java
class RetryExecutor (line 28) | @Component
method callWithRetry (line 44) | public <T> T callWithRetry(final String stepName, final Callable<T> ac...
method jitterRandom (line 77) | private long jitterRandom(final long jitter) {
FILE: ssl-service/src/main/java/tech/amak/portbuddy/sslservice/service/TransientErrorClassifier.java
class TransientErrorClassifier (line 32) | @UtilityClass
method isTransient (line 41) | public static boolean isTransient(final Exception e) {
FILE: ssl-service/src/main/java/tech/amak/portbuddy/sslservice/service/impl/ServerEmailService.java
class ServerEmailService (line 33) | @Service
method sendDnsInstructions (line 40) | @Override
FILE: ssl-service/src/main/java/tech/amak/portbuddy/sslservice/service/impl/SimpleDnsResolverService.java
class SimpleDnsResolverService (line 32) | @Service
method isTxtRecordVisible (line 36) | @Override
FILE: ssl-service/src/main/java/tech/amak/portbuddy/sslservice/web/CertificatesController.java
class CertificatesController (line 40) | @RestController
method createCertificate (line 55) | @PostMapping
method getCertificateByDomain (line 71) | @GetMapping("/{domain}")
method listCertificates (line 83) | @GetMapping
method createManagedCertificate (line 95) | @PostMapping("/managed")
method listManagedCertificates (line 117) | @GetMapping("/managed")
FILE: ssl-service/src/main/java/tech/amak/portbuddy/sslservice/web/ChallengeController.java
class ChallengeController (line 29) | @RestController
method getChallengeToken (line 41) | @GetMapping(value = "/.well-known/acme-challenge/{token}", produces = ...
FILE: ssl-service/src/main/java/tech/amak/portbuddy/sslservice/web/InternalController.java
class InternalController (line 31) | @RestController
method getCertificateByDomain (line 45) | @GetMapping("/{domain}")
method confirmDns (line 58) | @PostMapping("/jobs/{id}/confirm-dns")
FILE: ssl-service/src/main/java/tech/amak/portbuddy/sslservice/web/JobsController.java
class JobsController (line 34) | @RestController
method submitJob (line 50) | @PostMapping
method getJob (line 64) | @GetMapping("/{id}")
method listJobs (line 75) | @GetMapping
method confirmDns (line 86) | @PostMapping("/{id}/confirm-dns")
FILE: ssl-service/src/main/java/tech/amak/portbuddy/sslservice/work/ChallengeTokenStore.java
class ChallengeTokenStore (line 24) | @Component
method putToken (line 35) | public void putToken(final String token, final String content) {
method getTokenContent (line 45) | public String getTokenContent(final String token) {
method removeToken (line 54) | public void removeToken(final String token) {
FILE: ssl-service/src/main/resources/db/migration/V1__init.sql
type ssl_certificates (line 7) | CREATE TABLE IF NOT EXISTS ssl_certificates (
type ssl_certificate_jobs (line 25) | CREATE TABLE IF NOT EXISTS ssl_certificate_jobs (
type idx_ssl_certificate_jobs_domain (line 44) | CREATE INDEX IF NOT EXISTS idx_ssl_certificate_jobs_domain ON ssl_certif...
FILE: ssl-service/src/main/resources/db/migration/V2__shedlock_table.sql
type shedlock (line 6) | CREATE TABLE IF NOT EXISTS shedlock
FILE: ssl-service/src/test/java/tech/amak/portbuddy/sslservice/service/CertificateRenewalServiceTest.java
class CertificateRenewalServiceTest (line 39) | @ExtendWith(MockitoExtension.class)
method checkAndRenewCertificates_ShouldTriggerRenewalForExpiringCerts (line 54) | @Test
method checkAndRenewCertificates_NoExpiringCerts_ShouldDoNothing (line 73) | @Test
FILE: web/src/App.tsx
function ScrollToTop (line 42) | function ScrollToTop() {
function ScrollToHash (line 50) | function ScrollToHash() {
function App (line 71) | function App() {
FILE: web/src/auth/AuthContext.tsx
type User (line 5) | type User = {
constant ACCOUNT_NAME_CLAIM (line 22) | const ACCOUNT_NAME_CLAIM = 'aname'
constant ACCOUNT_ID_CLAIM (line 23) | const ACCOUNT_ID_CLAIM = 'aid'
type AuthState (line 25) | type AuthState = {
constant APP_ORIGIN (line 39) | const APP_ORIGIN = window.location.origin
constant OAUTH_REDIRECT_URI (line 40) | const OAUTH_REDIRECT_URI = `${APP_ORIGIN}/auth/callback`
function storeTokenFromUrlIfPresent (line 42) | function storeTokenFromUrlIfPresent(): string | null {
function AuthProvider (line 65) | function AuthProvider({ children }: { children: React.ReactNode }) {
function useAuth (line 212) | function useAuth(): AuthState {
FILE: web/src/components/AppLayout.tsx
type UserAccount (line 22) | type UserAccount = {
function AppLayout (line 30) | function AppLayout() {
type IconType (line 204) | type IconType = ComponentType<SVGProps<SVGSVGElement>>
function SideLink (line 206) | function SideLink({ to, label, end = false, Icon, onClick }: { to: strin...
function HeaderTitle (line 226) | function HeaderTitle() {
FILE: web/src/components/CodeBlock.tsx
function CodeBlock (line 16) | function CodeBlock({ code }: { code: string }) {
FILE: web/src/components/LoadingContext.tsx
type LoadingContextType (line 7) | interface LoadingContextType {
FILE: web/src/components/Modal.tsx
type ModalProps (line 5) | interface ModalProps {
function Modal (line 12) | function Modal({ isOpen, onClose, title, children }: ModalProps) {
type ConfirmModalProps (line 64) | interface ConfirmModalProps {
function ConfirmModal (line 75) | function ConfirmModal({
type AlertModalProps (line 115) | interface AlertModalProps {
function AlertModal (line 122) | function AlertModal({ isOpen, onClose, title, message }: AlertModalProps) {
FILE: web/src/components/PageHeader.tsx
type PageHeaderContextValue (line 3) | type PageHeaderContextValue = {
function PageHeaderProvider (line 10) | function PageHeaderProvider({ children }: PropsWithChildren) {
function usePageHeader (line 25) | function usePageHeader() {
function usePageTitle (line 31) | function usePageTitle(title: string) {
FILE: web/src/components/PlanComparison.tsx
function PlanComparison (line 24) | function PlanComparison() {
FILE: web/src/components/ProtectedRoute.tsx
function ProtectedRoute (line 5) | function ProtectedRoute({ children, role }: { children: React.ReactNode,...
FILE: web/src/lib/api.ts
function setLoadingCallbacks (line 7) | function setLoadingCallbacks(start: () => void, stop: () => void) {
function startLoading (line 12) | function startLoading() {
function stopLoading (line 16) | function stopLoading() {
constant API_BASE (line 20) | const API_BASE: string = (() => {
function getToken (line 29) | function getToken(): string | null {
function redirectToLogin (line 37) | function redirectToLogin(withFrom: boolean = true): void {
function withAuth (line 65) | function withAuth(init?: RequestInit, skipToken: boolean = false): Reque...
function apiJson (line 81) | async function apiJson<T = any>(path: string, init?: RequestInit, option...
function apiRaw (line 137) | async function apiRaw(path: string, init?: RequestInit): Promise<Respons...
FILE: web/src/lib/utils.ts
function formatDateTime (line 16) | function formatDateTime(dateString: string): string {
FILE: web/src/pages/AcceptInvite.tsx
function AcceptInvite (line 11) | function AcceptInvite() {
FILE: web/src/pages/Contacts.tsx
function Contacts (line 16) | function Contacts() {
FILE: web/src/pages/ForgotPassword.tsx
function ForgotPassword (line 10) | function ForgotPassword() {
FILE: web/src/pages/Installation.tsx
function Installation (line 10) | function Installation() {
function TabButton (line 208) | function TabButton({ isActive, onClick, label }: { isActive: boolean, on...
function Step (line 226) | function Step({ title, description, children }: { title: string, descrip...
function CodeBlock (line 236) | function CodeBlock({ code }: { code: string }) {
function InfoCard (line 262) | function InfoCard({ title, description }: { title: string, description: ...
function PlanLimitCard (line 276) | function PlanLimitCard({ plan, limit, description, isPro }: { plan: stri...
FILE: web/src/pages/Landing.tsx
function FeatureCard (line 38) | function FeatureCard({ icon, title, description }: { icon: React.ReactNo...
function Step (line 50) | function Step({ number, title, description }: { number: string, title: s...
function StatCard (line 62) | function StatCard({ label, value, icon }: { label: string, value: string...
function TestimonialCard (line 72) | function TestimonialCard({ quote, author, role }: { quote: string, autho...
function FaqItem (line 94) | function FaqItem({ question, answer }: { question: string, answer: strin...
function ChevronDownIcon (line 114) | function ChevronDownIcon({ className }: { className?: string }) {
function TypewriterText (line 122) | function TypewriterText() {
function Landing (line 165) | function Landing() {
FILE: web/src/pages/Login.tsx
function Login (line 6) | function Login() {
FILE: web/src/pages/NotFound.tsx
function NotFound (line 13) | function NotFound() {
FILE: web/src/pages/Passcode.tsx
function Passcode (line 8) | function Passcode() {
FILE: web/src/pages/Privacy.tsx
function Privacy (line 5) | function Privacy() {
FILE: web/src/pages/Register.tsx
function Register (line 10) | function Register() {
FILE: web/src/pages/ResetPassword.tsx
function ResetPassword (line 10) | function ResetPassword() {
FILE: web/src/pages/ServerError.tsx
function ServerError (line 9) | function ServerError() {
FILE: web/src/pages/Terms.tsx
function Terms (line 1) | function Terms() {
FILE: web/src/pages/app/AdminAccounts.tsx
type AdminAccountRow (line 24) | type AdminAccountRow = {
function AdminAccounts (line 34) | function AdminAccounts() {
FILE: web/src/pages/app/AdminPanel.tsx
type SystemStats (line 10) | type SystemStats = { totalUsers: number, activeTunnels: number, totalAcc...
type DailyStat (line 11) | type DailyStat = { date: string, newUsersCount: number, tunnelsCount: nu...
function AdminPanel (line 13) | function AdminPanel() {
FILE: web/src/pages/app/AdminTunnels.tsx
type AdminTunnelRow (line 24) | type AdminTunnelRow = {
function AdminTunnels (line 35) | function AdminTunnels() {
FILE: web/src/pages/app/AdminUsers.tsx
type AdminUserRow (line 24) | type AdminUserRow = {
function AdminUsers (line 34) | function AdminUsers() {
FILE: web/src/pages/app/Billing.tsx
function Billing (line 10) | function Billing() {
FILE: web/src/pages/app/BillingCancel.tsx
function BillingCancel (line 9) | function BillingCancel() {
FILE: web/src/pages/app/BillingSuccess.tsx
function BillingSuccess (line 9) | function BillingSuccess() {
FILE: web/src/pages/app/Domains.tsx
type Domain (line 8) | interface Domain {
function Domains (line 20) | function Domains() {
FILE: web/src/pages/app/Ports.tsx
type PortReservation (line 12) | interface PortReservation {
function Ports (line 21) | function Ports() {
FILE: web/src/pages/app/Profile.tsx
function Profile (line 11) | function Profile() {
FILE: web/src/pages/app/Settings.tsx
function Settings (line 7) | function Settings() {
FILE: web/src/pages/app/Team.tsx
type Member (line 12) | type Member = {
type Invitation (line 22) | type Invitation = {
function Team (line 30) | function Team() {
FILE: web/src/pages/app/Tokens.tsx
type TokenItem (line 14) | type TokenItem = { id: string, label: string, createdAt: string, revoked...
function Tokens (line 16) | function Tokens() {
function CopyButton (line 197) | function CopyButton({ text }: { text: string }) {
FILE: web/src/pages/app/Tunnels.tsx
type TunnelView (line 8) | type TunnelView = {
function Tunnels (line 24) | function Tunnels() {
FILE: web/src/pages/docs/DocsLayout.tsx
function DocsLayout (line 18) | function DocsLayout() {
function SidebarLink (line 77) | function SidebarLink({ to, label, active }: { to: string, label: string,...
FILE: web/src/pages/docs/DocsOverview.tsx
function DocsOverview (line 28) | function DocsOverview() {
function FeatureItem (line 246) | function FeatureItem({ icon, label }: { icon: React.ReactNode, label: st...
FILE: web/src/pages/docs/guides/HytaleGuide.tsx
function HytaleGuide (line 19) | function HytaleGuide() {
FILE: web/src/pages/docs/guides/MinecraftGuide.tsx
function MinecraftGuide (line 19) | function MinecraftGuide() {
FILE: web/vite.config.ts
method postProcess (line 24) | postProcess(renderedRoute) {
Condensed preview — 364 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,433K chars).
[
{
"path": ".github/workflows/ci.yml",
"chars": 10812,
"preview": "name: Build and Deploy\n\non:\n push:\n branches: [ \"**\" ]\n tags:\n - 'v*'\n - 'be*'\n - 'fe*'\n workflow"
},
{
"path": ".gitignore",
"chars": 584,
"preview": "target/\n!.mvn/wrapper/maven-wrapper.jar\n!**/src/main/**/target/\n!**/src/test/**/target/\n.kotlin\n\n### IntelliJ IDEA ###\n."
},
{
"path": ".mvn/wrapper/maven-wrapper.properties",
"chars": 168,
"preview": "wrapperVersion=3.3.4\ndistributionType=only-script\ndistributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/"
},
{
"path": "Dockerfile-cli",
"chars": 237,
"preview": "FROM eclipse-temurin:25-jre\n\nWORKDIR /app\nCOPY cli/target/cli-*.jar /app/app.jar\nCOPY entrypoint.sh /app/entrypoint.sh\nR"
},
{
"path": "Dockerfile-eureka",
"chars": 192,
"preview": "FROM eclipse-temurin:25-jre\n\nWORKDIR /app\nCOPY eureka/target/eureka-*.jar /app/app.jar\nCOPY entrypoint.sh /app/entrypoin"
},
{
"path": "Dockerfile-gateway",
"chars": 194,
"preview": "FROM eclipse-temurin:25-jre\n\nWORKDIR /app\nCOPY gateway/target/gateway-*.jar /app/app.jar\nCOPY entrypoint.sh /app/entrypo"
},
{
"path": "Dockerfile-net-proxy",
"chars": 198,
"preview": "FROM eclipse-temurin:25-jre\n\nWORKDIR /app\nCOPY net-proxy/target/net-proxy-*.jar /app/app.jar\nCOPY entrypoint.sh /app/ent"
},
{
"path": "Dockerfile-server",
"chars": 192,
"preview": "FROM eclipse-temurin:25-jre\n\nWORKDIR /app\nCOPY server/target/server-*.jar /app/app.jar\nCOPY entrypoint.sh /app/entrypoin"
},
{
"path": "Dockerfile-ssl-service",
"chars": 202,
"preview": "FROM eclipse-temurin:25-jre\n\nWORKDIR /app\nCOPY ssl-service/target/ssl-service-*.jar /app/app.jar\nCOPY entrypoint.sh /app"
},
{
"path": "Dockerfile-web",
"chars": 166,
"preview": "FROM alpine:latest\n\nWORKDIR /app\nCOPY web/dist /app/dist\nCOPY entrypoint-web.sh /app/entrypoint.sh\nRUN chmod +x /app/ent"
},
{
"path": "LICENSE",
"chars": 11358,
"preview": "\n Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 4926,
"preview": "# PortBuddy 🚀\n\nPortBuddy is a powerful yet simple tool that allows you to expose a port opened on your local host or in "
},
{
"path": "cli/pom.xml",
"chars": 7686,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n xmlns:xsi=\"http://www"
},
{
"path": "cli/src/main/java/tech/amak/portbuddy/cli/PortBuddy.java",
"chars": 23676,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "cli/src/main/java/tech/amak/portbuddy/cli/config/ConfigurationService.java",
"chars": 5396,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "cli/src/main/java/tech/amak/portbuddy/cli/tunnel/HttpTunnelClient.java",
"chars": 26225,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "cli/src/main/java/tech/amak/portbuddy/cli/tunnel/NetTunnelClient.java",
"chars": 26063,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "cli/src/main/java/tech/amak/portbuddy/cli/ui/ConsoleUi.java",
"chars": 9606,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "cli/src/main/java/tech/amak/portbuddy/cli/ui/HttpLogSink.java",
"chars": 710,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "cli/src/main/java/tech/amak/portbuddy/cli/ui/NetTrafficSink.java",
"chars": 715,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "cli/src/main/java/tech/amak/portbuddy/cli/utils/HttpUtils.java",
"chars": 3095,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "cli/src/main/java/tech/amak/portbuddy/cli/utils/JsonUtils.java",
"chars": 1026,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "cli/src/main/resources/META-INF/native-image/tech.amak/port-buddy-cli/reflect-config.json",
"chars": 3223,
"preview": "[\n {\n \"name\": \"tech.amak.portbuddy.common.dto.auth.RegisterRequest\",\n \"allDeclaredConstructors\": true,\n \"allPu"
},
{
"path": "cli/src/main/resources/META-INF/native-image/tech.amak/port-buddy-cli/resource-config.json",
"chars": 332,
"preview": "{\n \"resources\": [\n {\n \"pattern\": \"application\\\\.yml\"\n },\n {\n \"pattern\": \"application-dev\\\\.yml\"\n "
},
{
"path": "cli/src/main/resources/application-dev.yml",
"chars": 88,
"preview": "#serverUrl: \"https://localhost:8443\"\nserverUrl: https://localhost:8443\nlogEnabled: false"
},
{
"path": "cli/src/main/resources/application.yml",
"chars": 52,
"preview": "serverUrl: \"https://portbuddy.dev\"\nlogEnabled: true\n"
},
{
"path": "cli/src/main/resources/logback.xml",
"chars": 905,
"preview": "<!--\n ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n ~ you may not use this file except in complia"
},
{
"path": "common/pom.xml",
"chars": 1079,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n xmlns:xsi=\"http://www"
},
{
"path": "common/src/main/java/tech/amak/portbuddy/common/ClientConfig.java",
"chars": 1246,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "common/src/main/java/tech/amak/portbuddy/common/Plan.java",
"chars": 662,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "common/src/main/java/tech/amak/portbuddy/common/TunnelType.java",
"chars": 1559,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "common/src/main/java/tech/amak/portbuddy/common/dto/DnsInstructionsEmailRequest.java",
"chars": 1064,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "common/src/main/java/tech/amak/portbuddy/common/dto/ExposeRequest.java",
"chars": 968,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "common/src/main/java/tech/amak/portbuddy/common/dto/ExposeResponse.java",
"chars": 939,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "common/src/main/java/tech/amak/portbuddy/common/dto/auth/RegisterRequest.java",
"chars": 1092,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "common/src/main/java/tech/amak/portbuddy/common/dto/auth/RegisterResponse.java",
"chars": 1161,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "common/src/main/java/tech/amak/portbuddy/common/dto/auth/TokenExchangeRequest.java",
"chars": 1067,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "common/src/main/java/tech/amak/portbuddy/common/dto/auth/TokenExchangeResponse.java",
"chars": 1060,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "common/src/main/java/tech/amak/portbuddy/common/dto/jwks/JwkKey.java",
"chars": 1242,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "common/src/main/java/tech/amak/portbuddy/common/dto/jwks/JwksResponse.java",
"chars": 1005,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "common/src/main/java/tech/amak/portbuddy/common/tunnel/BinaryWsFrame.java",
"chars": 6474,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "common/src/main/java/tech/amak/portbuddy/common/tunnel/ControlMessage.java",
"chars": 1210,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "common/src/main/java/tech/amak/portbuddy/common/tunnel/HttpTunnelMessage.java",
"chars": 2298,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "common/src/main/java/tech/amak/portbuddy/common/tunnel/MessageEnvelope.java",
"chars": 1043,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "common/src/main/java/tech/amak/portbuddy/common/tunnel/WsTunnelMessage.java",
"chars": 2391,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "common/src/main/java/tech/amak/portbuddy/common/utils/IdUtils.java",
"chars": 2064,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "docker-compose.yml",
"chars": 4118,
"preview": "services:\n postgres:\n image: postgres:16-alpine\n container_name: pb-db\n restart: unless-stopped\n environmen"
},
{
"path": "entrypoint-cli-native.sh",
"chars": 35,
"preview": "#!/bin/sh\nexec /app/portbuddy \"$@\"\n"
},
{
"path": "entrypoint-web.sh",
"chars": 304,
"preview": "#!/bin/sh\necho \"Starting\" && \\\n ls -al /app/dist/ && \\\n mkdir -p /app/pb-web && \\\n echo \"Dir /app/pb-web/ (re)created"
},
{
"path": "entrypoint.sh",
"chars": 51,
"preview": "#!/bin/sh\nexec java $JVM_OPTS -jar /app/app.jar $@\n"
},
{
"path": "eureka/HELP.md",
"chars": 1169,
"preview": "# Getting Started\n\n### Reference Documentation\nFor further reference, please consider the following sections:\n\n* [Offici"
},
{
"path": "eureka/pom.xml",
"chars": 1526,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2"
},
{
"path": "eureka/src/main/java/tech/amak/portbuddy/eureka/EurekaApplication.java",
"chars": 1104,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "eureka/src/main/java/tech/amak/portbuddy/eureka/security/SecurityConfig.java",
"chars": 1625,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "eureka/src/main/resources/application.yml",
"chars": 547,
"preview": "server:\n port: 8761\n\neureka:\n instance:\n hostname: localhost\n client:\n service-url:\n defaultZone: http://$"
},
{
"path": "eureka/src/test/java/tech/amak/portbuddy/eureka/EurekaApplicationTests.java",
"chars": 790,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "gateway/HELP.md",
"chars": 2883,
"preview": "# Getting Started\n\n### Reference Documentation\nFor further reference, please consider the following sections:\n\n* [Offici"
},
{
"path": "gateway/pom.xml",
"chars": 3443,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2"
},
{
"path": "gateway/src/main/java/tech/amak/portbuddy/gateway/ApiGatewayApplication.java",
"chars": 1018,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "gateway/src/main/java/tech/amak/portbuddy/gateway/client/SslServiceClient.java",
"chars": 2244,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "gateway/src/main/java/tech/amak/portbuddy/gateway/config/AppProperties.java",
"chars": 1222,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "gateway/src/main/java/tech/amak/portbuddy/gateway/config/GlobalExceptionHandler.java",
"chars": 2559,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "gateway/src/main/java/tech/amak/portbuddy/gateway/config/LoadBalancerClientsConfig.java",
"chars": 1217,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "gateway/src/main/java/tech/amak/portbuddy/gateway/config/NetProxyLoadBalancerConfiguration.java",
"chars": 1824,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "gateway/src/main/java/tech/amak/portbuddy/gateway/config/PortBuddyServerLoadBalancerConfiguration.java",
"chars": 1878,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "gateway/src/main/java/tech/amak/portbuddy/gateway/config/SslServerConfig.java",
"chars": 6491,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "gateway/src/main/java/tech/amak/portbuddy/gateway/config/WebClientConfig.java",
"chars": 1037,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "gateway/src/main/java/tech/amak/portbuddy/gateway/dto/CertificateResponse.java",
"chars": 767,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "gateway/src/main/java/tech/amak/portbuddy/gateway/filter/PortBuddyRewritePathGatewayFilterFactory.java",
"chars": 4383,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "gateway/src/main/java/tech/amak/portbuddy/gateway/loadbalancer/NetProxyPublicHostLoadBalancer.java",
"chars": 5159,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "gateway/src/main/java/tech/amak/portbuddy/gateway/loadbalancer/PortBuddySubdomainLoadBalancer.java",
"chars": 6666,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "gateway/src/main/java/tech/amak/portbuddy/gateway/security/GatewayJwtConfig.java",
"chars": 1939,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "gateway/src/main/java/tech/amak/portbuddy/gateway/security/GatewaySecurityConfig.java",
"chars": 3345,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "gateway/src/main/java/tech/amak/portbuddy/gateway/ssl/DynamicSslProvider.java",
"chars": 8450,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "gateway/src/main/java/tech/amak/portbuddy/gateway/ssl/SniSslContextMapping.java",
"chars": 1515,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "gateway/src/main/resources/application.yml",
"chars": 8164,
"preview": "server:\n port: 8443\n\napp:\n http-port: 8080\n domain: localhost:${server.port}\n url: https://${app.domain}\n server-er"
},
{
"path": "gateway/src/test/java/tech/amak/gateway/ApiGatewayApplicationTests.java",
"chars": 1164,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "gateway/src/test/java/tech/amak/portbuddy/gateway/config/SslServerConfigTest.java",
"chars": 4146,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "gateway/src/test/java/tech/amak/portbuddy/gateway/ssl/DynamicSslProviderTest.java",
"chars": 3277,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "lombok.config",
"chars": 239,
"preview": "lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier\nlombok.copyableAnnotations += org.s"
},
{
"path": "mvnw",
"chars": 11790,
"preview": "#!/bin/sh\n# ----------------------------------------------------------------------------\n# Licensed to the Apache Softwa"
},
{
"path": "mvnw.cmd",
"chars": 8292,
"preview": "<# : batch portion\n@REM ----------------------------------------------------------------------------\n@REM Licensed to th"
},
{
"path": "net-proxy/pom.xml",
"chars": 2858,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n xmlns:xsi=\"http://www"
},
{
"path": "net-proxy/src/main/java/tech/amak/portbuddy/netproxy/NetProxyApplication.java",
"chars": 1290,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "net-proxy/src/main/java/tech/amak/portbuddy/netproxy/config/AppProperties.java",
"chars": 1209,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "net-proxy/src/main/java/tech/amak/portbuddy/netproxy/config/JwtConfig.java",
"chars": 2230,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "net-proxy/src/main/java/tech/amak/portbuddy/netproxy/config/WebSocketConfig.java",
"chars": 2465,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "net-proxy/src/main/java/tech/amak/portbuddy/netproxy/security/SecurityConfig.java",
"chars": 3231,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "net-proxy/src/main/java/tech/amak/portbuddy/netproxy/tunnel/NetTunnelRegistry.java",
"chars": 30494,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "net-proxy/src/main/java/tech/amak/portbuddy/netproxy/tunnel/NetTunnelWebSocketHandler.java",
"chars": 7853,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "net-proxy/src/main/java/tech/amak/portbuddy/netproxy/web/NetProxyController.java",
"chars": 2312,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "net-proxy/src/main/resources/application.yml",
"chars": 1171,
"preview": "server:\n port: ${NET_PROXY_PORT:8070}\n\neureka:\n client:\n registryFetchIntervalSeconds: 2\n eurekaServiceUrlPollIn"
},
{
"path": "net-proxy/src/test/java/tech/amak/portbuddy/netproxy/tunnel/NetTunnelLeakVerificationTest.java",
"chars": 4726,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "net-proxy/src/test/java/tech/amak/portbuddy/netproxy/tunnel/NetTunnelOrphanCleanupTest.java",
"chars": 3108,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "net-proxy/src/test/java/tech/amak/portbuddy/netproxy/tunnel/NetTunnelRegistryConcurrencyTest.java",
"chars": 3285,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "net-proxy/src/test/java/tech/amak/portbuddy/netproxy/tunnel/NetTunnelRegistryTest.java",
"chars": 6720,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "net-proxy/src/test/java/tech/amak/portbuddy/netproxy/tunnel/NetTunnelUdpEvictionTest.java",
"chars": 3414,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "pom.xml",
"chars": 5123,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n xmlns:xsi=\"http://www"
},
{
"path": "server/pom.xml",
"chars": 5163,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n xmlns:xsi=\"http://www"
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/ServerApplication.java",
"chars": 1166,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/client/NetProxyClient.java",
"chars": 2284,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/client/SslServiceClient.java",
"chars": 2081,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/config/AppProperties.java",
"chars": 2757,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/config/SchedulingConfig.java",
"chars": 1526,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/config/ThreatFoxProperties.java",
"chars": 1020,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/config/TunnelsProperties.java",
"chars": 1353,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/db/entity/AccountEntity.java",
"chars": 2344,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/db/entity/ApiKeyEntity.java",
"chars": 1767,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/db/entity/DomainEntity.java",
"chars": 2829,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/db/entity/InvitationEntity.java",
"chars": 1927,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/db/entity/PasswordResetTokenEntity.java",
"chars": 1659,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/db/entity/PortReservationEntity.java",
"chars": 2269,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/db/entity/Role.java",
"chars": 738,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/db/entity/StripeEventEntity.java",
"chars": 1573,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/db/entity/TunnelEntity.java",
"chars": 2984,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/db/entity/TunnelStatus.java",
"chars": 710,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/db/entity/UserAccountEntity.java",
"chars": 3787,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/db/entity/UserEntity.java",
"chars": 2114,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/db/repo/AccountRepository.java",
"chars": 3036,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/db/repo/ApiKeyRepository.java",
"chars": 1220,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/db/repo/DomainRepository.java",
"chars": 1846,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/db/repo/InvitationRepository.java",
"chars": 1213,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/db/repo/PasswordResetTokenRepository.java",
"chars": 1060,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/db/repo/PortReservationRepository.java",
"chars": 2882,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/db/repo/StripeEventRepository.java",
"chars": 827,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/db/repo/TunnelRepository.java",
"chars": 5078,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/db/repo/UserAccountRepository.java",
"chars": 1727,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/db/repo/UserRepository.java",
"chars": 2664,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/mail/EmailService.java",
"chars": 3123,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/mail/UserCreatedEvent.java",
"chars": 1011,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/mail/WelcomeEmailService.java",
"chars": 3791,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/security/ApiTokenAuthFilter.java",
"chars": 3702,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/security/JwtConfig.java",
"chars": 2574,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/security/JwtService.java",
"chars": 4379,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/security/Oauth2SuccessHandler.java",
"chars": 10544,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/security/RsaKeyProvider.java",
"chars": 5844,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/security/SecurityConfig.java",
"chars": 5535,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/security/ThreatBlockedException.java",
"chars": 764,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/service/ApiTokenService.java",
"chars": 6330,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/service/DomainService.java",
"chars": 17360,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/service/PaymentCleanupService.java",
"chars": 2845,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/service/PortReservationService.java",
"chars": 12183,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/service/ProxyDiscoveryService.java",
"chars": 2584,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/service/StaleTunnelsReaper.java",
"chars": 2385,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/service/StripeService.java",
"chars": 9932,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/service/StripeWebhookService.java",
"chars": 1393,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/service/TeamService.java",
"chars": 9456,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/service/TunnelService.java",
"chars": 14623,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/service/threatfox/ThreatFoxClient.java",
"chars": 1760,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/service/threatfox/ThreatFoxIoc.java",
"chars": 1251,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/service/threatfox/ThreatFoxRequest.java",
"chars": 691,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/service/threatfox/ThreatFoxResponse.java",
"chars": 946,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/service/threatfox/ThreatFoxService.java",
"chars": 5968,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/service/user/MissingEmailException.java",
"chars": 850,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/service/user/PasswordResetService.java",
"chars": 5828,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/service/user/UserProvisioningService.java",
"chars": 12746,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/tunnel/PermissiveSubprotocolHandshakeHandler.java",
"chars": 1376,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/tunnel/PublicWebSocketProxyHandler.java",
"chars": 13681,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/tunnel/TunnelRegistry.java",
"chars": 14767,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/tunnel/TunnelWebSocketHandler.java",
"chars": 6262,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/tunnel/WebSocketConfig.java",
"chars": 3058,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/AuthController.java",
"chars": 10763,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/DomainsController.java",
"chars": 8337,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/ExposeController.java",
"chars": 7704,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/IngressController.java",
"chars": 15667,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/IngressResolveController.java",
"chars": 3691,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/InternalDomainController.java",
"chars": 1537,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/InternalEmailController.java",
"chars": 2450,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/JwksController.java",
"chars": 2945,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/PaymentController.java",
"chars": 4823,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/PortsController.java",
"chars": 7536,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/StripeWebhookController.java",
"chars": 14713,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/TeamController.java",
"chars": 8055,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/TokensController.java",
"chars": 4375,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/TunnelStatusController.java",
"chars": 2415,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/TunnelsController.java",
"chars": 4614,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/UsersController.java",
"chars": 14118,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/admin/AdminAccountController.java",
"chars": 3916,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/admin/AdminSystemController.java",
"chars": 2762,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/admin/AdminTunnelController.java",
"chars": 3010,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/admin/AdminUserController.java",
"chars": 1967,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/admin/dto/AdminAccountRow.java",
"chars": 956,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/admin/dto/AdminStatsRow.java",
"chars": 1082,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/admin/dto/AdminTunnelRow.java",
"chars": 981,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/admin/dto/AdminUserRow.java",
"chars": 939,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/admin/dto/SystemStatsResponse.java",
"chars": 1004,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/advice/GlobalExceptionHandler.java",
"chars": 1951,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/dto/DomainDto.java",
"chars": 915,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/dto/LoginRequest.java",
"chars": 670,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/dto/PasswordResetConfirm.java",
"chars": 681,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/dto/PasswordResetRequest.java",
"chars": 661,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/dto/PortRangeDto.java",
"chars": 717,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/dto/PortReservationDto.java",
"chars": 841,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/dto/PortReservationUpdateRequest.java",
"chars": 848,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/dto/SetPasscodeRequest.java",
"chars": 804,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/dto/UpdateCustomDomainRequest.java",
"chars": 679,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/java/tech/amak/portbuddy/server/web/dto/UpdateDomainRequest.java",
"chars": 670,
"preview": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance "
},
{
"path": "server/src/main/resources/application.yml",
"chars": 4329,
"preview": "server:\n port: 8090\n compression:\n enabled: on\n\neureka:\n client:\n registryFetchIntervalSeconds: 2\n eurekaSer"
},
{
"path": "server/src/main/resources/db/migration/V10__link_http_tunnels_to_domain.sql",
"chars": 1001,
"preview": "ALTER TABLE tunnels ADD COLUMN domain_id UUID;\n\nALTER TABLE tunnels ADD CONSTRAINT fk_tunnels_domain_id FOREIGN KEY (dom"
},
{
"path": "server/src/main/resources/db/migration/V11__add_deleted_column_to_domains.sql",
"chars": 71,
"preview": "ALTER TABLE domains ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE;\n"
}
]
// ... and 164 more files (download for full content)
About this extraction
This page contains the full source code of the amak-tech/port-buddy GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 364 files (1.3 MB), approximately 313.1k tokens, and a symbol index with 992 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.