diff --git a/pom.xml b/pom.xml index 1edd34f..77a89e4 100644 --- a/pom.xml +++ b/pom.xml @@ -155,6 +155,31 @@ ${java.version} + + maven-jar-plugin + + + sb-client + + jar + + package + + sb-client + + **/SwitchboardExternalClient.class + **/SwitchboardExternalClient$$*.class + + + + true + org.aalku.joatse.cloud.tools.io.SwitchboardExternalClient + + + + + + diff --git a/src/main/java/org/aalku/joatse/cloud/config/ListenPortConfigurator.java b/src/main/java/org/aalku/joatse/cloud/config/ListenPortConfigurator.java index 6727282..d44d04c 100644 --- a/src/main/java/org/aalku/joatse/cloud/config/ListenPortConfigurator.java +++ b/src/main/java/org/aalku/joatse/cloud/config/ListenPortConfigurator.java @@ -1,6 +1,7 @@ package org.aalku.joatse.cloud.config; import java.net.InetAddress; +import java.net.UnknownHostException; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; @@ -21,6 +22,7 @@ @Configuration public class ListenPortConfigurator implements InitializingBean { + @Value("${cloud.port.open.range:}") private String openPortRangeString; @@ -73,7 +75,11 @@ PortRange httpUnsafePortRange(@Autowired @Qualifier("openPortRange") PortRange o @Bean("switchboardPortListener") AsyncTcpPortListener switchboardPortListener() { - return new AsyncTcpPortListener(InetAddress.getLoopbackAddress(), 0); + try { + return new AsyncTcpPortListener(InetAddress.getByName(System.getProperty("switchboard.host", "localhost")), Integer.getInteger("switchboard.port", 0)); + } catch (UnknownHostException e) { + throw new RuntimeException(e); + } } @Bean("httpHosts") diff --git a/src/main/java/org/aalku/joatse/cloud/config/WebSecurityConfiguration.java b/src/main/java/org/aalku/joatse/cloud/config/WebSecurityConfiguration.java index 45612ed..cdf8401 100644 --- a/src/main/java/org/aalku/joatse/cloud/config/WebSecurityConfiguration.java +++ b/src/main/java/org/aalku/joatse/cloud/config/WebSecurityConfiguration.java @@ -43,11 +43,12 @@ public class WebSecurityConfiguration { public static final String PATH_LOGIN_POST = "/loginPost"; public static final String PATH_POST_LOGIN = "/postLogin"; public static final String PATH_PASSWORD_RESET = "/resetPassword"; + public static final String PATH_PUBLIC = "/public/**"; @SuppressWarnings("unused") private Logger log = LoggerFactory.getLogger(WebSecurityConfiguration.class); - @Bean + @Bean // Must be public public ApplicationListener webInitializedListener() { return new ApplicationListener() { @@ -72,7 +73,7 @@ SecurityFilterChain filterChain(HttpSecurity http, JWTAuthorizationFilter jwtAut .addFilterBefore(jwtAuthorizationFilter, UsernamePasswordAuthenticationFilter.class) .authorizeHttpRequests(t -> t.requestMatchers(WebSocketConfig.CONNECTION_HTTP_PATH).anonymous() // - .requestMatchers(HttpMethod.GET, "/ws-test.html").permitAll() + .requestMatchers(HttpMethod.GET, PATH_PUBLIC).permitAll() .requestMatchers(HttpMethod.GET, "/login.html", "/css/**", "/header.js", "/lib/*.js").permitAll() .requestMatchers(HttpMethod.GET, "/user").permitAll() .requestMatchers("/error").permitAll() diff --git a/src/main/java/org/aalku/joatse/cloud/service/sharing/SharingManager.java b/src/main/java/org/aalku/joatse/cloud/service/sharing/SharingManager.java index 7b2d881..eb8bb2a 100644 --- a/src/main/java/org/aalku/joatse/cloud/service/sharing/SharingManager.java +++ b/src/main/java/org/aalku/joatse/cloud/service/sharing/SharingManager.java @@ -212,7 +212,7 @@ public TunnelCreationResponse requestTunnel(Principal principal, LotSharingReque } else { rejectConnectionRequest(request.getUuid(), "Invalid preconfirmation"); } - return new TunnelCreationResponse(request.getUuid(), null, request.future); + return new TunnelCreationResponse(request.getUuid(), null, request.getFuture()); } else if (automaticTunnelAcceptance != null) { // TODO remove this mock new Thread() { public void run() { @@ -226,10 +226,11 @@ public void run() { }; }.start(); } - return new TunnelCreationResponse(request.getUuid(), buildConfirmationUri(request), request.future); + return new TunnelCreationResponse(request.getUuid(), buildConfirmationUri(request), request.getFuture()); } private String checkPreconfirmation(PreconfirmedShare saved, LotSharingRequest request) { + // TODO autoAuthorizeByHttpUrl LotSharingRequest savedReq; try { savedReq = LotSharingRequest.fromJson(new JSONObject(saved.getResources()), saved.getRequesterAddress()); @@ -283,11 +284,11 @@ public void acceptTunnelRequest(UUID uuid, JoatseUser user) { } finally { lock.writeLock().unlock(); } - request.future.complete(newTunnel); // Without lock + request.getFuture().complete(newTunnel); // Without lock } catch (Exception e) { if (request != null) { // Reject - request.future.completeExceptionally(e); + request.getFuture().completeExceptionally(e); return; } else { throw e; @@ -332,7 +333,7 @@ public void rejectConnectionRequest(UUID uuid, String reason) { lock.writeLock().unlock(); } if (request != null) { - request.future.complete(new TunnelCreationResult.Rejected(request, reason)); // Out of lock + request.getFuture().complete(new TunnelCreationResult.Rejected(request, reason)); // Out of lock } } @@ -383,21 +384,33 @@ public void receivedTcpConnection(int port, AsynchronousSocketChannel channel, * A httpClientEnd connection has arrived. Returns a context object to be used * on the next call. */ - public Object httpClientEndConnected(UUID uuid, long targetId) { - HttpTunnel httpTarget = tunnelRegistry.getHttpTunnel(uuid, targetId); - if (httpTarget == null) { + public Object switchboardConnected(UUID uuid, long targetId) { + Object tunnel = tunnelRegistry.getTunnel(uuid, targetId); + if (tunnel == null) { log.warn("Unexpected connection or wrong protocol: {}.{}", uuid, targetId); return null; } else { log.info("Switchboard Received Tunnel connection: {}.{}", uuid, targetId); - return httpTarget; + return tunnel; } } /** The httpClientEnd connection is ready */ - public void httpClientEndConnectionReady(Object context, AsynchronousSocketChannel channel) { - HttpTunnel httpTarget = (HttpTunnel) context; - httpTarget.getTunnel().tunnelTcpConnection(httpTarget.getTargetId(), channel); + public void switchboardConnectionReady(Object context, AsynchronousSocketChannel channel) { + SharedResourceLot tunnel = null; + long targetId = -1; + if (context instanceof HttpTunnel) { + HttpTunnel httpTarget = (HttpTunnel) context; + tunnel = httpTarget.getTunnel(); + targetId = httpTarget.getTargetId(); + } else if (context instanceof TcpTunnel) { + TcpTunnel tcpTarget = (TcpTunnel) context; + tunnel = tcpTarget.getTunnel(); + targetId = tcpTarget.getTargetId(); + } else { + throw new RuntimeException(); + } + tunnel.tunnelTcpConnection(targetId, channel); // TODO schedule a periodic check to log the disconnection } @@ -411,14 +424,21 @@ public void httpClientEndConnectionReady(Object context, AsynchronousSocketChann public HttpTunnel getTunnelForHttpRequest(InetAddress remoteAddress, int serverPort, String serverName, String protocol) { List res = tunnelRegistry.findMatchingHttpTunnel(remoteAddress, serverPort, serverName, protocol); if (res.size() > 0) { - return res.get(0); + HttpTunnel ht = res.get(0); + SharedResourceLot srl = ht.getTunnel(); + /* If not authorized maybe it should be */ + if (!srl.getAllowedAddresses().contains(remoteAddress) + && srl.isAuthorizeByHttpUrl()) { + srl.addAllowedAddress(remoteAddress); + } + return ht; } else { return null; } } public HttpTunnel getHttpTunnelById(UUID uuid, long httpTunnelId) { - HttpTunnel res = tunnelRegistry.getHttpTunnel(uuid, httpTunnelId); + HttpTunnel res = tunnelRegistry.getTunnel(uuid, httpTunnelId); return res; } } diff --git a/src/main/java/org/aalku/joatse/cloud/service/sharing/TunnelRegistry.java b/src/main/java/org/aalku/joatse/cloud/service/sharing/TunnelRegistry.java index f045b4a..f65998f 100644 --- a/src/main/java/org/aalku/joatse/cloud/service/sharing/TunnelRegistry.java +++ b/src/main/java/org/aalku/joatse/cloud/service/sharing/TunnelRegistry.java @@ -4,7 +4,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.UUID; import java.util.stream.Collectors; @@ -26,9 +25,16 @@ public class TunnelRegistry { public synchronized List findMatchingHttpTunnel(InetAddress remoteAddress, int serverPort, String serverName, String protocol) { ListenAddress reachedAddress = new ListenAddress(serverPort, serverName, protocol); List tunnelsMatching = tunnelsByUUID.values().stream() - .filter(t -> t.getAllowedAddresses().contains(remoteAddress)) .flatMap(x -> x.getHttpItems().stream()) .filter(http->http.getListenAddress().equals(reachedAddress)) + .filter(http -> { + SharedResourceLot t = http.getTunnel(); + if (t.isAuthorizeByHttpUrl()) { + return true; + } else { + return t.getAllowedAddresses().contains(remoteAddress); + } + }) .collect(Collectors.toList()); return tunnelsMatching; } @@ -42,8 +48,18 @@ public synchronized List findMatchingTcpTunnel(InetAddress remoteAddr return tunnelsMatching; } - public synchronized HttpTunnel getHttpTunnel(UUID uuid, long targetId) { - return Optional.ofNullable(tunnelsByUUID.get(uuid)).map(t -> t.getHttpItem(targetId)).orElse(null); + @SuppressWarnings("unchecked") + public synchronized E getTunnel(UUID uuid, long targetId) { + SharedResourceLot srl = tunnelsByUUID.get(uuid); + if (srl == null) { + return null; + } else { + Object res = srl.getHttpItem(targetId); + if (res == null) { + res = srl.getTcpItem(targetId); + } + return (E) res; + } } public synchronized void removeTunnel(UUID uuid) { diff --git a/src/main/java/org/aalku/joatse/cloud/service/sharing/http/HttpEndpointGenerator.java b/src/main/java/org/aalku/joatse/cloud/service/sharing/http/HttpEndpointGenerator.java index fecb5c3..fe56cc8 100644 --- a/src/main/java/org/aalku/joatse/cloud/service/sharing/http/HttpEndpointGenerator.java +++ b/src/main/java/org/aalku/joatse/cloud/service/sharing/http/HttpEndpointGenerator.java @@ -12,11 +12,16 @@ import org.aalku.joatse.cloud.tools.io.PortRange; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; @Configuration public class HttpEndpointGenerator { + public enum PrefixStrategy { + URL, DESCRIPTION + } + private static final String DYNAMIC_PREFIX = "*."; @Autowired @@ -34,6 +39,12 @@ public class HttpEndpointGenerator { @Autowired private ListenerConfigurationDetector listenerConfigurationDetector; + @Value("${httpEndpointGenerator.prefixStrategy:DESCRIPTION}") + private PrefixStrategy prefixStrategy; + + @Value("${httpEndpointGenerator.prefixHashLength:6}") + private int prefixHashLength; + public ListenAddress generateListenAddress(HttpTunnel tunnel, LinkedHashSet forbiddenAddresses, String askedCloudHostname) { PortRange portRange = tunnel.isUnsafe() ? httpUnsafePortRange : httpPortRange; @@ -77,14 +88,27 @@ public ListenAddress generateListenAddress(HttpTunnel tunnel, LinkedHashSet p >=0 && p != 80 && p != 443).ifPresent(p->{ @@ -93,7 +117,7 @@ private static String generateSummaryPreffix(HttpTunnel tunnel) { return sb.toString(); } - private static String generateHashPrefix(HttpTunnel tunnel) { + private static String generateHashPrefix(HttpTunnel tunnel, int hashLength) { int targetPort = tunnel.getTargetPort(); try { MessageDigest md = MessageDigest.getInstance("SHA-1"); @@ -101,9 +125,12 @@ private static String generateHashPrefix(HttpTunnel tunnel) { md.update(new byte[] {(byte)(targetPort & 0xff), (byte)((targetPort >> 8) & 0xff)}); md.update(tunnel.getTargetURL().toExternalForm().getBytes(StandardCharsets.UTF_8)); byte[] digest = md.digest(); + if (hashLength > digest.length) { + throw new IllegalArgumentException("Selected hash algorithm can only generate " + digest.length + " chars"); + } StringBuilder sb = new StringBuilder(2); final String dictionary = "abcdefghmprstxyz"; - for (int i = 0; i < 4; i++) { + for (int i = 0; i < hashLength; i++) { char c = dictionary.charAt(digest[i] & 0x0F); sb.append(c); } diff --git a/src/main/java/org/aalku/joatse/cloud/service/sharing/http/HttpProxyManager.java b/src/main/java/org/aalku/joatse/cloud/service/sharing/http/HttpProxyManager.java index ddbd65a..90fb6ff 100644 --- a/src/main/java/org/aalku/joatse/cloud/service/sharing/http/HttpProxyManager.java +++ b/src/main/java/org/aalku/joatse/cloud/service/sharing/http/HttpProxyManager.java @@ -32,6 +32,7 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.Future; @@ -306,115 +307,38 @@ public void service(ServletRequest request, ServletResponse response) throws Ser try { HttpServletRequest servletRequest = (HttpServletRequest) request; HttpServletResponse servletResponse = (HttpServletResponse) response; - ServletContext servletContext = servletRequest.getServletContext(); - if (Optional.ofNullable(servletRequest.getHeader("Upgrade")).filter(x->x.equals("websocket")).isPresent()) { + // ID http tunnel + InetAddress remoteAddress = InetAddress.getByName(request.getRemoteAddr()); + int serverPort = request.getServerPort(); + String serverName = request.getServerName(); + String scheme = request.getScheme(); + HttpTunnel httpTunnel = sharingManager.getTunnelForHttpRequest(remoteAddress, serverPort, serverName, scheme); - InetAddress remoteAddress = InetAddress.getByName(request.getRemoteAddr()); - int serverPort = request.getServerPort(); - String serverName = request.getServerName(); - String scheme = request.getScheme(); - HttpTunnel httpTunnel = sharingManager.getTunnelForHttpRequest(remoteAddress, serverPort, serverName, scheme); - - if (httpTunnel == null) { - servletResponse.sendError(404); - return; - } - - JettyWebSocketServerContainer container = JettyWebSocketServerContainer - .getContainer(servletContext); - - URI mappedUri = setWebSocketScheme(new URI(rewriteUrl(new URL(servletRequest.getRequestURL().toString()), httpTunnel))); - - log.debug(String.format("WebSockets proxy connection to %s", mappedUri)); - ClientUpgradeRequest clientUpgradeRequest = newProxyClientUpgradeRequest(servletRequest, httpTunnel); - WebSocketClient client = newWsClient(); - - ServerSideWsHandler serverSideWsHandler = new ServerSideWsHandler(); - ClientSideWsHandler clientSideHandler = new ClientSideWsHandler(); - serverSideWsHandler.setCloseHandler(()->IOTools.runFailable(()->client.stop())); - clientSideHandler.getSession().thenAccept(s->{ - serverSideWsHandler.setTextMessageHandler(m->{ - try { - s.getRemote().sendString(m); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - }); - // TODO - }); - serverSideWsHandler.getSession().thenAccept(s->{ - clientSideHandler.setTextMessageHandler(m->{ - try { - s.getRemote().sendString(m); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - }); - // TODO - }); - - - CompletableFuture futureConnect = client.connect(clientSideHandler, mappedUri, clientUpgradeRequest); - boolean clientConnected = futureConnect.handle((s,e)->{ - if (e != null) { - IOTools.runFailable(()->client.stop()); - return false; - } else { - clientSideHandler.setCloseHandler(()->IOTools.runFailable(()->client.stop())); - return true; - } - }).get(); - if (clientConnected) { - boolean ok = container.upgrade((upgradeRequest, upgradeResponse) -> { - return serverSideWsHandler; - }, servletRequest, servletResponse); - if (!ok) { - IOTools.runFailable(()->client.stop()); - throw new IOException("Unable to upgrade WebSocket connection"); - } - } else { - int errorCode; - try { - errorCode = clientSideHandler.getErrorCode().get(2, TimeUnit.SECONDS); - } catch (TimeoutException x) { - log.error("Error waiting for an error event on a failed WS connection"); - errorCode = 500; - } - log.warn("Coudn't connect WS to server so we replicate the error code to the client: {}", errorCode); - servletResponse.sendError(errorCode); - return; - } + if (Optional.ofNullable(servletRequest.getHeader("Upgrade")).filter(x->x.equals("websocket")).isPresent()) { + websocketUpgrade(servletRequest, servletResponse, httpTunnel); return; } - - InetAddress remoteAddress = InetAddress.getByName(request.getRemoteAddr()); - HttpServletRequest httpServletRequest = (HttpServletRequest) request; - int serverPort = request.getServerPort(); - String serverName = request.getServerName(); - HttpTunnel httpTunnel = sharingManager.getTunnelForHttpRequest(remoteAddress, serverPort, serverName, request.getScheme()); + if (httpTunnel != null) { - log.info("Request {} {} for tunnel {}", httpServletRequest.getMethod(), - httpServletRequest.getRequestURL(), httpTunnel.getTargetId()); + log.info("Request {} {} for tunnel {}", servletRequest.getMethod(), + servletRequest.getRequestURL(), httpTunnel.getTargetId()); request.setAttribute(REQUEST_KEY_HTTPTUNNEL, httpTunnel); request.setAttribute(REQUEST_KEY_REWRITE_HEADERS, true); // TODO request.setAttribute(REQUEST_KEY_HIDE_PROXY, true); // TODO super.service(request, response); } else { - log.warn("Request {} {} rejected", httpServletRequest.getMethod(), - httpServletRequest.getRequestURL()); - // ((HttpServletResponse) response).sendError(404, "Unknown resource requested"); + log.warn("Request {} {} rejected: Unknown tunnel or unauthorized address", + servletRequest.getMethod(), servletRequest.getRequestURL()); HttpServletResponse hsr = (HttpServletResponse) response; try (PrintWriter pw = new PrintWriter(hsr.getOutputStream())) { hsr.setStatus(404); StringBuilder loginUrl = getPublicCloudUrl(); - String requestUrl = getRequestUrl(httpServletRequest); - List accepted = MediaType.parseMediaTypes(httpServletRequest.getHeader("Accept")); + String requestUrl = getRequestUrl(servletRequest); + List accepted = MediaType.parseMediaTypes(servletRequest.getHeader("Accept")); if (accepted.contains(MediaType.TEXT_HTML)) { - hsr.setContentType("text/html;charset=UTF-8"); + hsr.setContentType("text/html;charset=UTF-8"); pw.println("511 Network Authentication Required"); pw.println(""); @@ -442,6 +366,85 @@ public void service(ServletRequest request, ServletResponse response) throws Ser } } + private void websocketUpgrade(HttpServletRequest servletRequest, + HttpServletResponse servletResponse, HttpTunnel httpTunnel) + throws UnknownHostException, IOException, URISyntaxException, MalformedURLException, ServletException, + InterruptedException, ExecutionException { + + if (httpTunnel == null) { + servletResponse.sendError(404); + return; + } + + ServletContext servletContext = servletRequest.getServletContext(); + JettyWebSocketServerContainer container = JettyWebSocketServerContainer + .getContainer(servletContext); + + URI mappedUri = setWebSocketScheme(new URI(rewriteUrl(new URL(servletRequest.getRequestURL().toString()), httpTunnel))); + + log.debug(String.format("WebSockets proxy connection to %s", mappedUri)); + ClientUpgradeRequest clientUpgradeRequest = newProxyClientUpgradeRequest(servletRequest, httpTunnel); + WebSocketClient client = newWsClient(); + + ServerSideWsHandler serverSideWsHandler = new ServerSideWsHandler(); + ClientSideWsHandler clientSideHandler = new ClientSideWsHandler(); + serverSideWsHandler.setCloseHandler(()->IOTools.runFailable(()->client.stop())); + clientSideHandler.getSession().thenAccept(s->{ + serverSideWsHandler.setTextMessageHandler(m->{ + try { + s.getRemote().sendString(m); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + }); + // TODO + }); + serverSideWsHandler.getSession().thenAccept(s->{ + clientSideHandler.setTextMessageHandler(m->{ + try { + s.getRemote().sendString(m); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + }); + // TODO + }); + + + CompletableFuture futureConnect = client.connect(clientSideHandler, mappedUri, clientUpgradeRequest); + boolean clientConnected = futureConnect.handle((s,e)->{ + if (e != null) { + IOTools.runFailable(()->client.stop()); + return false; + } else { + clientSideHandler.setCloseHandler(()->IOTools.runFailable(()->client.stop())); + return true; + } + }).get(); + if (clientConnected) { + boolean ok = container.upgrade((upgradeRequest, upgradeResponse) -> { + return serverSideWsHandler; + }, servletRequest, servletResponse); + if (!ok) { + IOTools.runFailable(()->client.stop()); + throw new IOException("Unable to upgrade WebSocket connection"); + } + } else { + int errorCode; + try { + errorCode = clientSideHandler.getErrorCode().get(2, TimeUnit.SECONDS); + } catch (TimeoutException x) { + log.error("Error waiting for an error event on a failed WS connection"); + errorCode = 500; + } + log.warn("Coudn't connect WS to server so we replicate the error code to the client: {}", errorCode); + servletResponse.sendError(errorCode); + return; + } + } + private WebSocketClient newWsClient() throws ServletException { WebSocketClient client = new WebSocketClient(newHttpClient()); wsClients.add(new WeakReference(client)); diff --git a/src/main/java/org/aalku/joatse/cloud/service/sharing/request/LotSharingRequest.java b/src/main/java/org/aalku/joatse/cloud/service/sharing/request/LotSharingRequest.java index 8d10b25..44499d8 100644 --- a/src/main/java/org/aalku/joatse/cloud/service/sharing/request/LotSharingRequest.java +++ b/src/main/java/org/aalku/joatse/cloud/service/sharing/request/LotSharingRequest.java @@ -7,6 +7,7 @@ import java.time.Instant; import java.util.ArrayList; import java.util.Collection; +import java.util.LinkedHashSet; import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -21,14 +22,18 @@ public class LotSharingRequest { private final InetSocketAddress requesterAddress; private final Collection items; private final Instant creationTime; - public final CompletableFuture future = new CompletableFuture<>(); - private Set allowedAddresses; - private UUID preconfirmedUuid; + private final CompletableFuture future = new CompletableFuture<>(); + private final Set allowedAddresses; + private final UUID preconfirmedUuid; + private final boolean autoAuthorizeByHttpUrl; - public LotSharingRequest(InetSocketAddress connectionRequesterAddress, Collection tunnelItems) { + public LotSharingRequest(InetSocketAddress connectionRequesterAddress, Collection tunnelItems, boolean autoAuthorizeByHttpUrl, UUID preconfirmedUuid) { this.requesterAddress = connectionRequesterAddress; this.items = new ArrayList<>(tunnelItems); this.creationTime = Instant.now(); + this.allowedAddresses = new LinkedHashSet<>(); + this.preconfirmedUuid = preconfirmedUuid; + this.autoAuthorizeByHttpUrl = autoAuthorizeByHttpUrl; } public InetSocketAddress getRequesterAddress() { @@ -43,10 +48,6 @@ public CompletableFuture getFuture() { return future; } - public void setAllowedAddresses(Set allowedAddress) { - this.allowedAddresses = allowedAddress; - } - public UUID getUuid() { return uuid; } @@ -89,10 +90,11 @@ public static LotSharingRequest fromJson(JSONObject js, InetSocketAddress connec Collection items = new ArrayList(); items.addAll(tcpTunnelReqs); items.addAll(httpTunnelReqs); - LotSharingRequest lotSharingRequest = new LotSharingRequest(connectionRequesterAddress, items); - Optional.ofNullable(js.optString("preconfirmed")).filter(s->!s.isEmpty()).map(s->UUID.fromString(s)).ifPresent(uuid->{ - lotSharingRequest.setPreconfirmedUuid(uuid); - }); + + final UUID preconfirmedUuid = Optional.ofNullable(js.optString("preconfirmed")).filter(s->!s.isEmpty()).map(s->UUID.fromString(s)).orElse(null); + final boolean autoAuthorizeByHttpUrl = js.optBoolean("autoAuthorizeByHttpUrl", false); + LotSharingRequest lotSharingRequest = new LotSharingRequest(connectionRequesterAddress, items, autoAuthorizeByHttpUrl, preconfirmedUuid); + // TODO allowed addresses from json return lotSharingRequest; } @@ -101,8 +103,13 @@ public UUID getPreconfirmedUuid() { return preconfirmedUuid; } - public void setPreconfirmedUuid(UUID preconfirmedUuid) { - this.preconfirmedUuid = preconfirmedUuid; + public boolean isAutoAuthorizeByHttpUrl() { + return autoAuthorizeByHttpUrl; + } + + public void setAllowedAddresses(Set set) { + allowedAddresses.addAll(set); + allowedAddresses.retainAll(set); } diff --git a/src/main/java/org/aalku/joatse/cloud/service/sharing/shared/SharedResourceLot.java b/src/main/java/org/aalku/joatse/cloud/service/sharing/shared/SharedResourceLot.java index 3cb67cb..ec5dd64 100644 --- a/src/main/java/org/aalku/joatse/cloud/service/sharing/shared/SharedResourceLot.java +++ b/src/main/java/org/aalku/joatse/cloud/service/sharing/shared/SharedResourceLot.java @@ -40,6 +40,7 @@ public class SharedResourceLot { private BiConsumer tcpConnectionListener; private Collection tcpItems = new ArrayList<>(1); private Collection httpItems = new ArrayList<>(1); + private boolean authorizeByHttpUrl; public SharedResourceLot(JoatseUser owner, LotSharingRequest request, String cloudPublicHostname) { this.owner = owner; @@ -61,6 +62,8 @@ public SharedResourceLot(JoatseUser owner, LotSharingRequest request, String clo for (TunnelRequestHttpItem r : httpItems) { this.addHttpItem(r); } + + this.authorizeByHttpUrl = request.isAutoAuthorizeByHttpUrl(); } @@ -207,4 +210,8 @@ public JSONObject toJsonSharedResources() { } return res; } + + public boolean isAuthorizeByHttpUrl() { + return authorizeByHttpUrl; + } } \ No newline at end of file diff --git a/src/main/java/org/aalku/joatse/cloud/service/sharing/shared/TcpTunnel.java b/src/main/java/org/aalku/joatse/cloud/service/sharing/shared/TcpTunnel.java index 34d5c3b..aad7e28 100644 --- a/src/main/java/org/aalku/joatse/cloud/service/sharing/shared/TcpTunnel.java +++ b/src/main/java/org/aalku/joatse/cloud/service/sharing/shared/TcpTunnel.java @@ -32,4 +32,7 @@ public int getListenPort() { public void setListenPort(int listenPort) { this.listenPort = listenPort; } + public long getTargetId() { + return targetId; + } } \ No newline at end of file diff --git a/src/main/java/org/aalku/joatse/cloud/tools/io/Switchboard.java b/src/main/java/org/aalku/joatse/cloud/tools/io/Switchboard.java index 1fc1124..d73651b 100644 --- a/src/main/java/org/aalku/joatse/cloud/tools/io/Switchboard.java +++ b/src/main/java/org/aalku/joatse/cloud/tools/io/Switchboard.java @@ -60,7 +60,7 @@ public class Switchboard implements InitializingBean, DisposableBean, Consumer switchboardPortListener; @Autowired - private SharingManager sharingManager; // TODO the tunnel will run through it. + private SharingManager sharingManager; // the tunnel will run through it. // @Autowired // private TunnelRegistry tunnelRegistry; @@ -94,14 +94,14 @@ private void handleNewChannel(AsynchronousSocketChannel channel) { UUID uuid = new UUID(headerBuffer.getLong(), headerBuffer.getLong()); long targetId = headerBuffer.getLong(); - Object context = sharingManager.httpClientEndConnected(uuid, targetId); + Object context = sharingManager.switchboardConnected(uuid, targetId); if (context != null) { try { IOTools.asyncWriteWholeBuffer(channel, ByteBuffer.wrap(new byte[] {0})) // send 0 = OK .thenAccept(x -> { try { log.info("Switchboard proxy is ready: {}.{}", uuid, targetId); - sharingManager.httpClientEndConnectionReady(context, channel); + sharingManager.switchboardConnectionReady(context, channel); } catch (Exception e1) { reportErrorThenClose(channel, 2, e1.toString()); } diff --git a/src/main/java/org/aalku/joatse/cloud/tools/io/SwitchboardExternalClient.java b/src/main/java/org/aalku/joatse/cloud/tools/io/SwitchboardExternalClient.java new file mode 100644 index 0000000..558ed16 --- /dev/null +++ b/src/main/java/org/aalku/joatse/cloud/tools/io/SwitchboardExternalClient.java @@ -0,0 +1,140 @@ +package org.aalku.joatse.cloud.tools.io; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.SocketChannel; +import java.nio.channels.WritableByteChannel; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +/** + * This main class allows us to use the Switchboard system from the command + * line. + * + * The process will connect to the specified (switchboard) port and it will tell + * it to connect to certain tunnel and target, and then it will redirect stdin + * to the socket and the socket to stdout. + * + * You could use it like this to pass an SSH connection through a tunnel: + * + *
+ * ssh -o 'ProxyCommand java -jar file.jar    ' @ ...
+ * 
+ * + * The is needed in the command line but it's not really used, just to associate the pub key to that name. + * + */ +public class SwitchboardExternalClient { + + private static final int BUFFER_SIZE = 1024*128; + + public static void main(String[] args) { + try { + String host = args[0]; + int port = Integer.parseInt(args[1], 10); + UUID uuid = UUID.fromString(args[2]); + long targetId = Long.parseLong(args[3], 10); + + // Connect + SocketChannel client = SocketChannel.open(); + client.configureBlocking(true); + client.connect(new InetSocketAddress(host, port)); + // Say hello + ByteBuffer hello = ByteBuffer.allocate(16 + 8); + hello.putLong(uuid.getMostSignificantBits()); + hello.putLong(uuid.getLeastSignificantBits()); + hello.putLong(targetId); + hello.flip(); + while (hello.hasRemaining()) { + client.write(hello); + } + hello.flip(); + hello = ByteBuffer.allocate(1); + int x; + if ((x = client.read(hello)) != 1) { + throw new IOException("Couldn't read switchboard response: " + x); + } + hello.flip(); + if (hello.get() != 0) { + hello = ByteBuffer.allocate(BUFFER_SIZE); + while (client.read(hello) >= 0); + hello.flip(); + hello.limit(hello.limit() - 1); // \0 at the end + throw new IOException("Negative response from switchboard: " + hello.asCharBuffer().toString()); + } + hello = null; + + // Two way comm until exception or both ends closed + CompletableFuture resOut = copyThread(Channels.newChannel(System.in), client, "thOut"); + CompletableFuture resIn = copyThread(client, Channels.newChannel(System.out), "thIn"); + CompletableFuture.allOf(resIn, resOut).exceptionally(e->{ + resIn.cancel(true); + resOut.cancel(true); + return null; + }).get(); + + try { + client.close(); + } catch (Exception e) { + } + } catch (Exception e) { + e.printStackTrace(System.err); + System.exit(1); + } + + } + + private static CompletableFuture copyThread(ReadableByteChannel in, WritableByteChannel out, String name) { + CompletableFuture res = new CompletableFuture(); + new Thread(name) { + volatile boolean closed = false; + + { + this.setDaemon(true); + this.start(); + res.exceptionally(e->{ + try { + in.close(); + } catch (Exception e1) { + } + try { + out.close(); + } catch (Exception e1) { + } + closed = true; + this.interrupt(); + return null; + }); + } + public void run() { + ByteBuffer buff = ByteBuffer.allocate(BUFFER_SIZE); + while (true) { + try { + int c = (!closed && buff.capacity() > 0) ? in.read(buff) : 0; + int p = buff.position(); + if (c < 0) { + closed = true; + if (p == 0) { + res.complete(null); + return; + } + } + if (p > 0) { + buff.flip(); + out.write(buff); + buff.compact(); + } + } catch (IOException e) { + res.completeExceptionally(e); + return; + } + } + }; + }; + return res; + } + +} diff --git a/src/main/java/org/aalku/joatse/cloud/web/ConfirmController.java b/src/main/java/org/aalku/joatse/cloud/web/ConfirmController.java index 05c4e29..e060d2a 100644 --- a/src/main/java/org/aalku/joatse/cloud/web/ConfirmController.java +++ b/src/main/java/org/aalku/joatse/cloud/web/ConfirmController.java @@ -36,6 +36,7 @@ public class ConfirmController { public final static String CONFIRM_SESSION_KEY_HASH = "CONFIRM_SESSION_KEY_HASH"; + @SuppressWarnings("unused") private Logger log = LoggerFactory.getLogger(ConfirmController.class); @Autowired diff --git a/src/main/java/org/aalku/joatse/cloud/web/HeaderController.java b/src/main/java/org/aalku/joatse/cloud/web/HeaderController.java index c3e3ff5..859c335 100644 --- a/src/main/java/org/aalku/joatse/cloud/web/HeaderController.java +++ b/src/main/java/org/aalku/joatse/cloud/web/HeaderController.java @@ -15,6 +15,7 @@ @Controller public class HeaderController { + @SuppressWarnings("unused") private Logger log = LoggerFactory.getLogger(HeaderController.class); @Autowired