From 86844528487fb50690faf142c06b9c1acf2fa110 Mon Sep 17 00:00:00 2001 From: Zlatin Balevsky Date: Mon, 28 Oct 2019 14:48:38 +0000 Subject: [PATCH] Add ability to limit the total number of upload slots, as well as per user --- .../main/groovy/com/muwire/core/Core.groovy | 2 +- .../com/muwire/core/MuWireSettings.groovy | 6 ++ .../muwire/core/upload/UploadManager.groovy | 63 ++++++++++++++++++- .../com/muwire/gui/OptionsController.groovy | 10 +++ .../models/com/muwire/gui/OptionsModel.groovy | 4 ++ .../views/com/muwire/gui/OptionsView.groovy | 17 ++++- 6 files changed, 97 insertions(+), 5 deletions(-) diff --git a/core/src/main/groovy/com/muwire/core/Core.groovy b/core/src/main/groovy/com/muwire/core/Core.groovy index 77b92d4e..bd50242d 100644 --- a/core/src/main/groovy/com/muwire/core/Core.groovy +++ b/core/src/main/groovy/com/muwire/core/Core.groovy @@ -276,7 +276,7 @@ public class Core { eventBus.register(UIDownloadResumedEvent.class, downloadManager) log.info("initializing upload manager") - uploadManager = new UploadManager(eventBus, fileManager, meshManager, downloadManager) + uploadManager = new UploadManager(eventBus, fileManager, meshManager, downloadManager, props) log.info("initializing connection establisher") connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache) diff --git a/core/src/main/groovy/com/muwire/core/MuWireSettings.groovy b/core/src/main/groovy/com/muwire/core/MuWireSettings.groovy index aa5943da..f8811f35 100644 --- a/core/src/main/groovy/com/muwire/core/MuWireSettings.groovy +++ b/core/src/main/groovy/com/muwire/core/MuWireSettings.groovy @@ -17,6 +17,8 @@ class MuWireSettings { int trustListInterval Set trustSubscriptions int downloadRetryInterval + int totalUploadSlots + int uploadSlotsPerUser int updateCheckInterval boolean autoDownloadUpdate String updateType @@ -73,6 +75,8 @@ class MuWireSettings { searchComments = Boolean.valueOf(props.getProperty("searchComments","true")) browseFiles = Boolean.valueOf(props.getProperty("browseFiles","true")) speedSmoothSeconds = Integer.valueOf(props.getProperty("speedSmoothSeconds","60")) + totalUploadSlots = Integer.valueOf(props.getProperty("totalUploadSlots","-1")) + uploadSlotsPerUser = Integer.valueOf(props.getProperty("uploadSlotsPerUser","-1")) watchedDirectories = readEncodedSet(props, "watchedDirectories") watchedKeywords = readEncodedSet(props, "watchedKeywords") @@ -118,6 +122,8 @@ class MuWireSettings { props.setProperty("searchComments", String.valueOf(searchComments)) props.setProperty("browseFiles", String.valueOf(browseFiles)) props.setProperty("speedSmoothSeconds", String.valueOf(speedSmoothSeconds)) + props.setProperty("totalUploadSlots", String.valueOf(totalUploadSlots)) + props.setProperty("uploadSlotsPerUser", String.valueOf(uploadSlotsPerUser)) writeEncodedSet(watchedDirectories, "watchedDirectories", props) writeEncodedSet(watchedKeywords, "watchedKeywords", props) diff --git a/core/src/main/groovy/com/muwire/core/upload/UploadManager.groovy b/core/src/main/groovy/com/muwire/core/upload/UploadManager.groovy index 46540218..f4cdf36a 100644 --- a/core/src/main/groovy/com/muwire/core/upload/UploadManager.groovy +++ b/core/src/main/groovy/com/muwire/core/upload/UploadManager.groovy @@ -4,6 +4,8 @@ import java.nio.charset.StandardCharsets import com.muwire.core.EventBus import com.muwire.core.InfoHash +import com.muwire.core.MuWireSettings +import com.muwire.core.Persona import com.muwire.core.SharedFile import com.muwire.core.connection.Endpoint import com.muwire.core.download.DownloadManager @@ -22,15 +24,22 @@ public class UploadManager { private final FileManager fileManager private final MeshManager meshManager private final DownloadManager downloadManager + private final MuWireSettings props + /** LOCKING: this on both structures */ + private int totalUploads + private final Map uploadsPerUser = new HashMap<>() + public UploadManager() {} public UploadManager(EventBus eventBus, FileManager fileManager, - MeshManager meshManager, DownloadManager downloadManager) { + MeshManager meshManager, DownloadManager downloadManager, + MuWireSettings props) { this.eventBus = eventBus this.fileManager = fileManager this.meshManager = meshManager this.downloadManager = downloadManager + this.props = props } public void processGET(Endpoint e) throws IOException { @@ -82,7 +91,15 @@ public class UploadManager { if (request.have > 0) eventBus.publish(new SourceDiscoveredEvent(infoHash : request.infoHash, source : request.downloader)) - + + if (!incrementUploads(request.downloader)) { + log.info("rejecting due to slot limit") + e.getOutputStream().write("429 Too Many Requests\r\n\r\n".getBytes(StandardCharsets.US_ASCII)) + e.getOutputStream().flush() + e.close() + return + } + Mesh mesh File file int pieceSize @@ -103,6 +120,7 @@ public class UploadManager { try { uploader.respond() } finally { + decrementUploads(request.downloader) eventBus.publish(new UploadFinishedEvent(uploader : uploader)) } } @@ -157,12 +175,21 @@ public class UploadManager { return } } + + if (!incrementUploads(request.downloader)) { + log.info("rejecting due to slot limit") + e.getOutputStream().write("429 Too Many Requests\r\n\r\n".getBytes(StandardCharsets.US_ASCII)) + e.getOutputStream().flush() + e.close() + return + } Uploader uploader = new HashListUploader(e, fullInfoHash, request) eventBus.publish(new UploadEvent(uploader : uploader)) try { uploader.respond() } finally { + decrementUploads(request.downloader) eventBus.publish(new UploadFinishedEvent(uploader : uploader)) } @@ -233,5 +260,37 @@ public class UploadManager { } } } + + /** + * @param p downloader + * @return true if this upload hasn't hit any slot limits + */ + private synchronized boolean incrementUploads(Persona p) { + if (props.totalUploadSlots >= 0 && totalUploads >= props.totalUploadSlots) + return false + if (props.uploadSlotsPerUser == 0) + return false + + Integer currentUploads = uploadsPerUser.get(p) + if (currentUploads == null) + currentUploads = 0 + if (props.uploadSlotsPerUser > 0 && currentUploads >= props.uploadSlotsPerUser) + return false + uploadsPerUser.put(p, ++currentUploads) + totalUploads++ + true + } + + private synchronized void decrementUploads(Persona p) { + totalUploads-- + Integer currentUploads = uploadsPerUser.get(p) + if (currentUploads == null || currentUploads == 0) + throw new IllegalStateException() + currentUploads-- + if (currentUploads == 0) + uploadsPerUser.remove(p) + else + uploadsPerUser.put(p, currentUploads) + } } diff --git a/gui/griffon-app/controllers/com/muwire/gui/OptionsController.groovy b/gui/griffon-app/controllers/com/muwire/gui/OptionsController.groovy index bb44ec88..52d2a6f0 100644 --- a/gui/griffon-app/controllers/com/muwire/gui/OptionsController.groovy +++ b/gui/griffon-app/controllers/com/muwire/gui/OptionsController.groovy @@ -69,6 +69,16 @@ class OptionsController { text = view.updateField.text model.updateCheckInterval = text settings.updateCheckInterval = Integer.valueOf(text) + + text = view.totalUploadSlotsField.text + int totalUploadSlots = Integer.valueOf(text) + model.totalUploadSlots = totalUploadSlots + settings.totalUploadSlots = totalUploadSlots + + text = view.uploadSlotsPerUserField.text + int uploadSlotsPerUser = Integer.valueOf(text) + model.uploadSlotsPerUser = uploadSlotsPerUser + settings.uploadSlotsPerUser = uploadSlotsPerUser boolean searchComments = view.searchCommentsCheckbox.model.isSelected() model.searchComments = searchComments diff --git a/gui/griffon-app/models/com/muwire/gui/OptionsModel.groovy b/gui/griffon-app/models/com/muwire/gui/OptionsModel.groovy index 05f88835..7417dd3b 100644 --- a/gui/griffon-app/models/com/muwire/gui/OptionsModel.groovy +++ b/gui/griffon-app/models/com/muwire/gui/OptionsModel.groovy @@ -19,6 +19,8 @@ class OptionsModel { @Observable boolean searchComments @Observable boolean browseFiles @Observable int speedSmoothSeconds + @Observable int totalUploadSlots + @Observable int uploadSlotsPerUser // i2p options @Observable String inboundLength @@ -65,6 +67,8 @@ class OptionsModel { searchComments = settings.searchComments browseFiles = settings.browseFiles speedSmoothSeconds = settings.speedSmoothSeconds + totalUploadSlots = settings.totalUploadSlots + uploadSlotsPerUser = settings.uploadSlotsPerUser Core core = application.context.get("core") inboundLength = core.i2pOptions["inbound.length"] diff --git a/gui/griffon-app/views/com/muwire/gui/OptionsView.groovy b/gui/griffon-app/views/com/muwire/gui/OptionsView.groovy index 326b55c4..26c6a847 100644 --- a/gui/griffon-app/views/com/muwire/gui/OptionsView.groovy +++ b/gui/griffon-app/views/com/muwire/gui/OptionsView.groovy @@ -42,6 +42,8 @@ class OptionsView { def searchCommentsCheckbox def browseFilesCheckbox def speedSmoothSecondsField + def totalUploadSlotsField + def uploadSlotsPerUserField def inboundLengthField def inboundQuantityField @@ -107,8 +109,19 @@ class OptionsView { button(text : "Choose", constraints : gbc(gridx : 2, gridy:2), incompleteLocationAction) } + panel (border : titledBorder(title : "Upload Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP, + constraints : gbc(gridx : 0, gridy:2, fill : GridBagConstraints.HORIZONTAL))) { + gridBagLayout() + label(text : "Total upload slots (-1 means unlimited)", constraints : gbc(gridx: 0, gridy : 0, anchor : GridBagConstraints.LINE_START, weightx: 100)) + totalUploadSlotsField = textField(text : bind {model.totalUploadSlots}, columns: 2, + constraints : gbc(gridx : 1, gridy: 0, anchor : GridBagConstraints.LINE_END)) + label(text : "Upload slots per user (-1 means unlimited)", constraints : gbc(gridx: 0, gridy : 1, anchor : GridBagConstraints.LINE_START, weightx: 100)) + uploadSlotsPerUserField = textField(text : bind {model.uploadSlotsPerUser}, columns: 2, + constraints : gbc(gridx : 1, gridy: 1, anchor : GridBagConstraints.LINE_END)) + } + panel (border : titledBorder(title : "Sharing Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP, - constraints : gbc(gridx : 0, gridy : 2, fill : GridBagConstraints.HORIZONTAL))) { + constraints : gbc(gridx : 0, gridy : 3, fill : GridBagConstraints.HORIZONTAL))) { gridBagLayout() label(text : "Share downloaded files", constraints : gbc(gridx : 0, gridy:0, anchor : GridBagConstraints.LINE_START, weightx : 100)) shareDownloadedCheckbox = checkBox(selected : bind {model.shareDownloadedFiles}, constraints : gbc(gridx :1, gridy:0, weightx : 0)) @@ -118,7 +131,7 @@ class OptionsView { } panel (border : titledBorder(title : "Update Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP, - constraints : gbc(gridx : 0, gridy : 3, fill : GridBagConstraints.HORIZONTAL))) { + constraints : gbc(gridx : 0, gridy : 4, fill : GridBagConstraints.HORIZONTAL))) { gridBagLayout() label(text : "Check for updates every (hours)", constraints : gbc(gridx : 0, gridy: 0, anchor : GridBagConstraints.LINE_START, weightx : 100)) updateField = textField(text : bind {model.updateCheckInterval }, columns : 2, constraints : gbc(gridx : 1, gridy: 0, weightx: 0))