synchronized void clear() {
   downloads.clear();
   uploads.clear();
   requestsByIdentifier.clear();
   downloadsByURI.clear();
   uploadsByFinalURI.clear();
 }
 /**
  * Restart a download. Caller should call ,false first, at which point we setStarted, and ,true
  * when it has actually started (a race condition means we don't setStarted at that point since
  * it's possible the success/failure callback might happen first).
  *
  * @param redirect If non-null, the request followed a redirect.
  */
 public synchronized void updateStarted(String identifier, FreenetURI redirect) {
   DownloadRequestStatus status = (DownloadRequestStatus) requestsByIdentifier.get(identifier);
   if (status == null) return; // Can happen during cancel etc.
   status.restart(false);
   if (redirect != null) {
     downloadsByURI.remove(status.getURI());
     status.redirect(redirect);
     downloadsByURI.put(redirect, status);
   }
 }
Example #3
0
 private void sendMethodNotAllowed(String method, boolean shouldDisconnect)
     throws ToadletContextClosedException, IOException {
   if (closed) throw new ToadletContextClosedException();
   MultiValueTable<String, String> mvt = new MultiValueTable<String, String>();
   mvt.put("Allow", "GET, PUT");
   sendError(
       sockOutputStream,
       405,
       "Method Not Allowed",
       l10n("methodNotAllowed"),
       shouldDisconnect,
       mvt);
 }
 synchronized void removeByIdentifier(String identifier) {
   RequestStatus status = requestsByIdentifier.remove(identifier);
   if (status == null) return;
   if (status instanceof DownloadRequestStatus) {
     downloads.remove(status);
     FreenetURI uri = status.getURI();
     assert (uri != null);
     downloadsByURI.removeElement(uri, status);
   } else if (status instanceof UploadRequestStatus) {
     uploads.remove(status);
     FreenetURI uri = ((UploadRequestStatus) status).getFinalURI();
     if (uri != null) uploadsByFinalURI.removeElement(uri, status);
   }
 }
 synchronized void gotFinalURI(String identifier, FreenetURI finalURI) {
   UploadRequestStatus status = (UploadRequestStatus) requestsByIdentifier.get(identifier);
   if (status == null) return; // Can happen during cancel etc.
   if (status.getFinalURI() == null)
     // No final URI set yet, put into the index.
     uploadsByFinalURI.put(finalURI, status);
   status.setFinalURI(finalURI);
 }
 synchronized void addDownload(DownloadRequestStatus status) {
   RequestStatus old = requestsByIdentifier.put(status.getIdentifier(), status);
   if (logMINOR) Logger.minor(this, "Starting download " + status.getIdentifier());
   if (old == status) return;
   if (old != null) downloads.remove(old);
   downloads.add(status);
   downloadsByURI.put(status.getURI(), status);
 }
 synchronized void addUpload(UploadRequestStatus status) {
   RequestStatus old = requestsByIdentifier.put(status.getIdentifier(), status);
   if (old == status) return;
   if (logMINOR) Logger.minor(this, "Starting upload " + status.getIdentifier());
   if (old != null) uploads.remove(old);
   uploads.add(status);
   FreenetURI uri = status.getURI();
   if (uri != null) uploadsByFinalURI.put(uri, status);
 }
Example #8
0
  /**
   * Should the connection be closed after handling this request?
   *
   * @param isHTTP10 Did the client specify HTTP/1.0?
   * @param headers Client headers.
   * @return True if the connection should be closed.
   */
  private static boolean shouldDisconnectAfterHandled(
      boolean isHTTP10, MultiValueTable<String, String> headers) {
    String connection = headers.get("connection");
    if (connection != null) {
      if (connection.equalsIgnoreCase("close")) return true;

      if (connection.equalsIgnoreCase("keep-alive")) return false;
    }
    if (isHTTP10 == true) return true;
    else
      // HTTP 1.1
      return false;
  }
 synchronized void finishedUpload(
     String identifier,
     boolean success,
     FreenetURI finalURI,
     InsertExceptionMode failureCode,
     String failureReasonShort,
     String failureReasonLong) {
   UploadRequestStatus status = (UploadRequestStatus) requestsByIdentifier.get(identifier);
   if (status == null) return; // Can happen during cancel etc.
   if (status.getFinalURI() == null && finalURI != null)
     // No final URI set yet, put into the index.
     uploadsByFinalURI.put(finalURI, status);
   status.setFinished(success, finalURI, failureCode, failureReasonShort, failureReasonLong);
 }
 public synchronized CacheFetchResult getShadowBucket(FreenetURI key, boolean noFilter) {
   Object[] downloads = downloadsByURI.getArray(key);
   if (downloads == null) return null;
   for (Object o : downloads) {
     DownloadRequestStatus download = (DownloadRequestStatus) o;
     Bucket data = download.getDataShadow();
     if (data == null) continue;
     if (data.size() == 0) continue;
     if (noFilter && download.filterData) continue;
     // FIXME it probably *is* worth the effort to allow this when it is overridden on the fetcher,
     // since the user changed the type???
     if (download.overriddenDataType) continue;
     return new CacheFetchResult(
         new ClientMetadata(download.getMIMEType()), new NoFreeBucket(data), download.filterData);
   }
   return null;
 }
Example #11
0
  /** Handle an incoming connection. Blocking, obviously. */
  public static void handle(Socket sock, ToadletContainer container, PageMaker pageMaker) {
    try {
      InputStream is = new BufferedInputStream(sock.getInputStream(), 4096);

      LineReadingInputStream lis = new LineReadingInputStream(is);

      while (true) {

        String firstLine = lis.readLine(32768, 128, false); // ISO-8859-1 or US-ASCII, _not_ UTF-8
        if (firstLine == null) {
          sock.close();
          return;
        } else if (firstLine.equals("")) {
          continue;
        }

        boolean logMINOR = Logger.shouldLog(Logger.MINOR, ToadletContextImpl.class);

        if (logMINOR) Logger.minor(ToadletContextImpl.class, "first line: " + firstLine);

        String[] split = firstLine.split(" ");

        if (split.length != 3)
          throw new ParseException(
              "Could not parse request line (split.length=" + split.length + "): " + firstLine);

        if (!split[2].startsWith("HTTP/1."))
          throw new ParseException("Unrecognized protocol " + split[2]);

        URI uri;
        try {
          uri = URIPreEncoder.encodeURI(split[1]).normalize();
          if (logMINOR)
            Logger.minor(
                ToadletContextImpl.class,
                "URI: "
                    + uri
                    + " path "
                    + uri.getPath()
                    + " host "
                    + uri.getHost()
                    + " frag "
                    + uri.getFragment()
                    + " port "
                    + uri.getPort()
                    + " query "
                    + uri.getQuery()
                    + " scheme "
                    + uri.getScheme());
        } catch (URISyntaxException e) {
          sendURIParseError(sock.getOutputStream(), true, e);
          return;
        }

        String method = split[0];

        MultiValueTable<String, String> headers = new MultiValueTable<String, String>();

        while (true) {
          String line = lis.readLine(32768, 128, false); // ISO-8859 or US-ASCII, not UTF-8
          if (line == null) {
            sock.close();
            return;
          }
          // System.out.println("Length="+line.length()+": "+line);
          if (line.length() == 0) break;
          int index = line.indexOf(':');
          if (index < 0) {
            throw new ParseException("Missing ':' in request header field");
          }
          String before = line.substring(0, index).toLowerCase();
          String after = line.substring(index + 1);
          after = after.trim();
          headers.put(before, after);
        }

        boolean disconnect =
            shouldDisconnectAfterHandled(split[2].equals("HTTP/1.0"), headers)
                || !container.enablePersistentConnections();

        boolean allowPost = container.allowPosts();
        BucketFactory bf = container.getBucketFactory();

        ToadletContextImpl ctx = new ToadletContextImpl(sock, headers, bf, pageMaker, container);
        ctx.shouldDisconnect = disconnect;

        /*
         * copy the data into a bucket now,
         * before we go into the redirect loop
         */

        Bucket data;

        boolean methodIsConfigurable = true;

        String slen = headers.get("content-length");

        if (METHODS_MUST_HAVE_DATA.contains(method)) {
          // <method> must have data
          methodIsConfigurable = false;
          if (slen == null) {
            ctx.shouldDisconnect = true;
            ctx.sendReplyHeaders(400, "Bad Request", null, null, -1);
            return;
          }
        } else if (METHODS_CANNOT_HAVE_DATA.contains(method)) {
          // <method> can not have data
          methodIsConfigurable = false;
          if (slen != null) {
            ctx.shouldDisconnect = true;
            ctx.sendReplyHeaders(400, "Bad Request", null, null, -1);
            return;
          }
        }

        if (slen != null) {
          long len;
          try {
            len = Integer.parseInt(slen);
            if (len < 0) throw new NumberFormatException("content-length less than 0");
          } catch (NumberFormatException e) {
            ctx.shouldDisconnect = true;
            ctx.sendReplyHeaders(400, "Bad Request", null, null, -1);
            return;
          }
          if (allowPost && ((!container.publicGatewayMode()) || ctx.isAllowedFullAccess())) {
            data = bf.makeBucket(len);
            BucketTools.copyFrom(data, is, len);
          } else {
            FileUtil.skipFully(is, len);
            if (method.equals("POST")) {
              ctx.sendMethodNotAllowed("POST", true);
            } else {
              sendError(
                  sock.getOutputStream(),
                  403,
                  "Forbidden",
                  "Content not allowed in this configuration",
                  true,
                  null);
            }
            ctx.close();
            return;
          }
        } else {
          // we're not doing to use it, but we have to keep
          // the compiler happy
          data = null;
        }

        if (!container.enableExtendedMethodHandling()) {
          if (!METHODS_RESTRICTED_MODE.contains(method)) {
            sendError(
                sock.getOutputStream(),
                403,
                "Forbidden",
                "Method not allowed in this configuration",
                true,
                null);
            return;
          }
        }

        // Handle it.
        try {
          boolean redirect = true;
          while (redirect) {
            // don't go around the loop unless set explicitly
            redirect = false;

            Toadlet t;
            try {
              t = container.findToadlet(uri);
            } catch (PermanentRedirectException e) {
              Toadlet.writePermanentRedirect(ctx, "Found elsewhere", e.newuri.toASCIIString());
              break;
            }

            if (t == null) {
              ctx.sendNoToadletError(ctx.shouldDisconnect);
              break;
            }

            // if the Toadlet does not support the method, we don't need to parse the data
            // also due this pre check a 'NoSuchMethodException' should never appear
            if (!(t.findSupportedMethods().contains(method))) {
              ctx.sendMethodNotAllowed(method, ctx.shouldDisconnect);
              break;
            }

            HTTPRequestImpl req = new HTTPRequestImpl(uri, data, ctx, method);
            try {
              String methodName = "handleMethod" + method;
              try {
                Class<? extends Toadlet> c = t.getClass();
                Method m = c.getMethod(methodName, HANDLE_PARAMETERS);
                if (methodIsConfigurable) {
                  AllowData anno = m.getAnnotation(AllowData.class);
                  if (anno == null) {
                    if (data != null) {
                      sendError(
                          sock.getOutputStream(),
                          400,
                          "Bad Request",
                          "Content not allowed",
                          true,
                          null);
                      ctx.close();
                      return;
                    }
                  } else if (anno.value()) {
                    if (data == null) {
                      sendError(
                          sock.getOutputStream(),
                          400,
                          "Bad Request",
                          "Missing Content",
                          true,
                          null);
                      ctx.close();
                      return;
                    }
                  }
                }
                ctx.setActiveToadlet(t);
                Object arglist[] = new Object[] {uri, req, ctx};
                m.invoke(t, arglist);
              } catch (InvocationTargetException ite) {
                throw ite.getCause();
              }
            } catch (RedirectException re) {
              uri = re.newuri;
              redirect = true;
            } finally {
              req.freeParts();
            }
          }
          if (ctx.shouldDisconnect) {
            sock.close();
            return;
          }
        } finally {
          if (data != null) data.free();
        }
      }

    } catch (ParseException e) {
      try {
        sendError(
            sock.getOutputStream(),
            400,
            "Bad Request",
            l10n("parseErrorWithError", "error", e.getMessage()),
            true,
            null);
      } catch (IOException e1) {
        // Ignore
      }
    } catch (TooLongException e) {
      try {
        sendError(
            sock.getOutputStream(), 400, "Bad Request", l10n("headersLineTooLong"), true, null);
      } catch (IOException e1) {
        // Ignore
      }
    } catch (IOException e) {
      // ignore and return
    } catch (ToadletContextClosedException e) {
      Logger.error(
          ToadletContextImpl.class, "ToadletContextClosedException while handling connection!");
    } catch (Throwable t) {
      Logger.error(ToadletContextImpl.class, "Caught error: " + t + " handling socket", t);
      try {
        sendError(sock.getOutputStream(), 500, "Internal Error", t.toString(), true, null);
      } catch (IOException e1) {
        // ignore and return
      }
    }
  }
Example #12
0
  static void sendReplyHeaders(
      OutputStream sockOutputStream,
      int replyCode,
      String replyDescription,
      MultiValueTable<String, String> mvt,
      String mimeType,
      long contentLength,
      Date mTime,
      boolean disconnect)
      throws IOException {
    // Construct headers
    if (mvt == null) mvt = new MultiValueTable<String, String>();
    if (mimeType != null)
      if (mimeType.equalsIgnoreCase("text/html")) {
        mvt.put("content-type", mimeType + "; charset=UTF-8");
      } else {
        mvt.put("content-type", mimeType);
      }
    if (contentLength >= 0) mvt.put("content-length", Long.toString(contentLength));

    String expiresTime;
    if (mTime == null) {
      expiresTime = "Thu, 01 Jan 1970 00:00:00 GMT";
    } else {
      // use an expiry time of 1 day, somewhat arbitrarily
      expiresTime = TimeUtil.makeHTTPDate(mTime.getTime() + (24 * 60 * 60 * 1000));
    }
    mvt.put("expires", expiresTime);

    String nowString = TimeUtil.makeHTTPDate(System.currentTimeMillis());
    String lastModString;
    if (mTime == null) {
      lastModString = nowString;
    } else {
      lastModString = TimeUtil.makeHTTPDate(mTime.getTime());
    }

    mvt.put("last-modified", lastModString);
    mvt.put("date", nowString);
    if (mTime == null) {
      mvt.put("pragma", "no-cache");
      mvt.put(
          "cache-control",
          "max-age=0, must-revalidate, no-cache, no-store, post-check=0, pre-check=0");
    }
    if (disconnect) mvt.put("connection", "close");
    else mvt.put("connection", "keep-alive");
    StringBuilder buf = new StringBuilder(1024);
    buf.append("HTTP/1.1 ");
    buf.append(replyCode);
    buf.append(' ');
    buf.append(replyDescription);
    buf.append("\r\n");
    for (Enumeration<String> e = mvt.keys(); e.hasMoreElements(); ) {
      String key = e.nextElement();
      Object[] list = mvt.getArray(key);
      key = fixKey(key);
      for (int i = 0; i < list.length; i++) {
        String val = (String) list[i];
        buf.append(key);
        buf.append(": ");
        buf.append(val);
        buf.append("\r\n");
      }
    }
    buf.append("\r\n");
    sockOutputStream.write(buf.toString().getBytes("US-ASCII"));
  }
  /** Handle an incoming connection. Blocking, obviously. */
  public static void handle(Socket sock, ToadletContainer container, PageMaker pageMaker) {
    try {
      InputStream is = new BufferedInputStream(sock.getInputStream(), 4096);

      LineReadingInputStream lis = new LineReadingInputStream(is);

      while (true) {

        String firstLine = lis.readLine(32768, 128, false); // ISO-8859-1 or US-ASCII, _not_ UTF-8
        if (firstLine == null) {
          sock.close();
          return;
        } else if (firstLine.equals("")) {
          continue;
        }

        boolean logMINOR = Logger.shouldLog(Logger.MINOR, ToadletContextImpl.class);

        if (logMINOR) Logger.minor(ToadletContextImpl.class, "first line: " + firstLine);

        String[] split = firstLine.split(" ");

        if (split.length != 3)
          throw new ParseException(
              "Could not parse request line (split.length=" + split.length + "): " + firstLine);

        if (!split[2].startsWith("HTTP/1."))
          throw new ParseException("Unrecognized protocol " + split[2]);

        URI uri;
        try {
          uri = URIPreEncoder.encodeURI(split[1]).normalize();
          if (logMINOR)
            Logger.minor(
                ToadletContextImpl.class,
                "URI: "
                    + uri
                    + " path "
                    + uri.getPath()
                    + " host "
                    + uri.getHost()
                    + " frag "
                    + uri.getFragment()
                    + " port "
                    + uri.getPort()
                    + " query "
                    + uri.getQuery()
                    + " scheme "
                    + uri.getScheme());
        } catch (URISyntaxException e) {
          sendURIParseError(sock.getOutputStream(), true, e);
          return;
        }

        String method = split[0];

        MultiValueTable<String, String> headers = new MultiValueTable<String, String>();

        while (true) {
          String line = lis.readLine(32768, 128, false); // ISO-8859 or US-ASCII, not UTF-8
          if (line == null) {
            sock.close();
            return;
          }
          // System.out.println("Length="+line.length()+": "+line);
          if (line.length() == 0) break;
          int index = line.indexOf(':');
          if (index < 0) {
            throw new ParseException("Missing ':' in request header field");
          }
          String before = line.substring(0, index).toLowerCase();
          String after = line.substring(index + 1);
          after = after.trim();
          headers.put(before, after);
        }

        boolean disconnect =
            shouldDisconnectAfterHandled(split[2].equals("HTTP/1.0"), headers)
                || !container.enablePersistentConnections();

        boolean allowPost = container.allowPosts();
        BucketFactory bf = container.getBucketFactory();

        ToadletContextImpl ctx = new ToadletContextImpl(sock, headers, bf, pageMaker, container);
        ctx.shouldDisconnect = disconnect;

        /*
         * if we're handling a POST, copy the data into a bucket now,
         * before we go into the redirect loop
         */

        Bucket data;

        if (method.equals("POST")) {
          String slen = headers.get("content-length");
          if (slen == null) {
            sendError(
                sock.getOutputStream(),
                400,
                "Bad Request",
                l10n("noContentLengthInPOST"),
                true,
                null);
            return;
          }
          long len;
          try {
            len = Integer.parseInt(slen);
            if (len < 0) throw new NumberFormatException("content-length less than 0");
          } catch (NumberFormatException e) {
            sendError(
                sock.getOutputStream(),
                400,
                "Bad Request",
                l10n("cannotParseContentLengthWithError", "error", e.toString()),
                true,
                null);
            return;
          }
          if (allowPost && ((!container.publicGatewayMode()) || ctx.isAllowedFullAccess())) {

            data = bf.makeBucket(len);
            BucketTools.copyFrom(data, is, len);
          } else {
            FileUtil.skipFully(is, len);
            ctx.sendMethodNotAllowed("POST", true);
            ctx.close();
            return;
          }
        } else {
          // we're not doing to use it, but we have to keep
          // the compiler happy
          data = null;
        }

        // Handle it.
        try {
          boolean redirect = true;
          while (redirect) {
            // don't go around the loop unless set explicitly
            redirect = false;

            Toadlet t;
            try {
              t = container.findToadlet(uri);
            } catch (PermanentRedirectException e) {
              Toadlet.writePermanentRedirect(ctx, "Found elsewhere", e.newuri.toASCIIString());
              break;
            }

            if (t == null) {
              ctx.sendNoToadletError(ctx.shouldDisconnect);
              break;
            }

            HTTPRequestImpl req = new HTTPRequestImpl(uri, data, ctx, method);
            try {
              if (method.equals("GET")) {
                ctx.setActiveToadlet(t);
                t.handleGet(uri, req, ctx);
                ctx.close();
              } else if (method.equals("POST")) {
                ctx.setActiveToadlet(t);
                t.handlePost(uri, req, ctx);
              } else {
                ctx.sendMethodNotAllowed(method, ctx.shouldDisconnect);
                ctx.close();
              }
            } catch (RedirectException re) {
              uri = re.newuri;
              redirect = true;
            } finally {
              req.freeParts();
            }
          }
          if (ctx.shouldDisconnect) {
            sock.close();
            return;
          }
        } finally {
          if (data != null) data.free();
        }
      }

    } catch (ParseException e) {
      try {
        sendError(
            sock.getOutputStream(),
            400,
            "Bad Request",
            l10n("parseErrorWithError", "error", e.getMessage()),
            true,
            null);
      } catch (IOException e1) {
        // Ignore
      }
    } catch (TooLongException e) {
      try {
        sendError(
            sock.getOutputStream(), 400, "Bad Request", l10n("headersLineTooLong"), true, null);
      } catch (IOException e1) {
        // Ignore
      }
    } catch (IOException e) {
      // ignore and return
    } catch (ToadletContextClosedException e) {
      Logger.error(
          ToadletContextImpl.class, "ToadletContextClosedException while handling connection!");
    } catch (Throwable t) {
      Logger.error(ToadletContextImpl.class, "Caught error: " + t + " handling socket", t);
      try {
        sendError(sock.getOutputStream(), 500, "Internal Error", t.toString(), true, null);
      } catch (IOException e1) {
        // ignore and return
      }
    }
  }
  public void handleMethodPOST(URI uri, HTTPRequest request, ToadletContext ctx)
      throws ToadletContextClosedException, IOException, RedirectException {

    if (!ctx.checkFullAccess(this)) return;

    // User requested reset to defaults, so present confirmation page.
    if (request.isPartSet("confirm-reset-to-defaults")) {
      PageNode page = ctx.getPageMaker().getPageNode(l10n("confirmResetTitle"), ctx);
      HTMLNode pageNode = page.outer;
      HTMLNode contentNode = page.content;

      HTMLNode content =
          ctx.getPageMaker()
              .getInfobox(
                  "infobox-warning", l10n("confirmResetTitle"), contentNode, "reset-confirm", true);
      content.addChild("#", l10n("confirmReset"));

      HTMLNode formNode = ctx.addFormChild(content, path(), "yes-button");
      String subconfig = request.getPartAsStringFailsafe("subconfig", MAX_PARAM_VALUE_SIZE);
      formNode.addChild(
          "input",
          new String[] {"type", "name", "value"},
          new String[] {"hidden", "subconfig", subconfig});

      // Persist visible fields so that they are reset to default or
      // unsaved changes are persisted.
      for (String part : request.getParts()) {
        if (part.startsWith(subconfig)) {
          formNode.addChild(
              "input",
              new String[] {"type", "name", "value"},
              new String[] {
                "hidden", part, request.getPartAsStringFailsafe(part, MAX_PARAM_VALUE_SIZE)
              });
        }
      }

      formNode.addChild(
          "input",
          new String[] {"type", "name", "value"},
          new String[] {
            "submit", "reset-to-defaults", NodeL10n.getBase().getString("Toadlet.yes")
          });

      formNode.addChild(
          "input",
          new String[] {"type", "name", "value"},
          new String[] {
            "submit", "decline-default-reset", NodeL10n.getBase().getString("Toadlet.no")
          });
      writeHTMLReply(ctx, 200, "OK", pageNode.generate());
      return;
    }

    // Returning from directory selector with a selection or declining
    // resetting settings to defaults.
    // Re-render config page with any changes made in the selector and/or
    // persisting values changed but
    // not applied.
    if (request.isPartSet(LocalFileBrowserToadlet.selectDir)
        || request.isPartSet("decline-default-reset")) {
      handleMethodGET(uri, request, ctx);
      return;
    }

    // Entering directory selector from config page.
    // This would be two loops if it checked for a redirect
    // (key.startsWith("select-directory.")) before
    // constructing params string. It always constructs it, then redirects
    // if it turns out to be needed.
    boolean directorySelector = false;
    StringBuilder paramsBuilder = new StringBuilder();
    paramsBuilder.append('?');
    String value;
    for (String key : request.getParts()) {
      // Prepare parts for page selection redirect:
      // Extract option and put into "select-for"; preserve others.
      value = request.getPartAsStringFailsafe(key, MAX_PARAM_VALUE_SIZE);
      if (key.startsWith("select-directory.")) {
        paramsBuilder
            .append("select-for=")
            .append(URLEncoder.encode(key.substring("select-directory.".length()), true))
            .append('&');
        directorySelector = true;
      } else {
        paramsBuilder
            .append(URLEncoder.encode(key, true))
            .append('=')
            .append(URLEncoder.encode(value, true))
            .append('&');
      }
    }
    String params = paramsBuilder.toString();
    if (directorySelector) {
      MultiValueTable<String, String> headers = new MultiValueTable<String, String>(1);
      // params ends in &. Download directory browser starts in default
      // download directory.
      headers.put(
          "Location",
          directoryBrowserPath + params + "path=" + core.getDownloadsDir().getAbsolutePath());
      ctx.sendReplyHeaders(302, "Found", headers, null, 0);
      return;
    }

    StringBuilder errbuf = new StringBuilder();
    boolean logMINOR = Logger.shouldLog(LogLevel.MINOR, this);

    String prefix = request.getPartAsStringFailsafe("subconfig", MAX_PARAM_VALUE_SIZE);
    if (logMINOR) {
      Logger.minor(this, "Current config prefix is " + prefix);
    }
    boolean resetToDefault = request.isPartSet("reset-to-defaults");
    if (resetToDefault && logMINOR) {
      Logger.minor(this, "Resetting to defaults");
    }

    for (Option<?> o : config.get(prefix).getOptions()) {
      String configName = o.getName();
      if (logMINOR) {
        Logger.minor(this, "Checking option " + prefix + '.' + configName);
      }

      // This ignores unrecognized parameters.
      if (request.isPartSet(prefix + '.' + configName)) {
        // Current subconfig is to be reset to default.
        if (resetToDefault) {
          // Disallow resetting fproxy port number to default as it
          // might break the link to start fproxy on the system tray,
          // shortcuts etc.
          if (prefix.equals("fproxy") && configName.equals("port")) continue;
          value = o.getDefault();
        } else {
          value = request.getPartAsStringFailsafe(prefix + '.' + configName, MAX_PARAM_VALUE_SIZE);
        }

        if (!(o.getValueDisplayString().equals(value))) {

          if (logMINOR) {
            Logger.minor(this, "Changing " + prefix + '.' + configName + " to " + value);
          }

          try {
            o.setValue(value);
          } catch (InvalidConfigValueException e) {
            errbuf.append(o.getName()).append(' ').append(e.getMessage()).append('\n');
          } catch (NodeNeedRestartException e) {
            needRestart = true;
          } catch (Exception e) {
            errbuf.append(o.getName()).append(' ').append(e).append('\n');
            Logger.error(this, "Caught " + e, e);
          }
        } else if (logMINOR) {
          Logger.minor(this, prefix + '.' + configName + " not changed");
        }
      }
    }

    // Wrapper params
    String wrapperConfigName = "wrapper.java.maxmemory";
    if (request.isPartSet(wrapperConfigName)) {
      value = request.getPartAsStringFailsafe(wrapperConfigName, MAX_PARAM_VALUE_SIZE);
      if (!WrapperConfig.getWrapperProperty(wrapperConfigName).equals(value)) {
        if (logMINOR) {
          Logger.minor(this, "Setting " + wrapperConfigName + " to " + value);
        }
        WrapperConfig.setWrapperProperty(wrapperConfigName, value);
      }
    }

    config.store();

    PageNode page = ctx.getPageMaker().getPageNode(l10n("appliedTitle"), ctx);
    HTMLNode pageNode = page.outer;
    HTMLNode contentNode = page.content;

    if (errbuf.length() == 0) {
      HTMLNode content =
          ctx.getPageMaker()
              .getInfobox(
                  "infobox-success",
                  l10n("appliedTitle"),
                  contentNode,
                  "configuration-applied",
                  true);
      content.addChild("#", l10n("appliedSuccess"));

      if (needRestart) {
        content.addChild("br");
        content.addChild("#", l10n("needRestart"));

        if (node.isUsingWrapper()) {
          content.addChild("br");
          HTMLNode restartForm = ctx.addFormChild(content, "/", "restartForm");
          restartForm.addChild(
              "input", //
              new String[] {"type", "name"}, //
              new String[] {"hidden", "restart"});
          restartForm.addChild(
              "input", //
              new String[] {"type", "name", "value"}, //
              new String[] {
                "submit",
                "restart2", //
                l10n("restartNode")
              });
        }

        if (needRestartUserAlert == null) {
          needRestartUserAlert = new NeedRestartUserAlert(ctx.getFormPassword());
          ctx.getAlertManager().register(needRestartUserAlert);
        }
      }
    } else {
      HTMLNode content =
          ctx.getPageMaker()
              .getInfobox(
                  "infobox-error",
                  l10n("appliedFailureTitle"),
                  contentNode,
                  "configuration-error",
                  true)
              .addChild("div", "class", "infobox-content");
      content.addChild("#", l10n("appliedFailureExceptions"));
      content.addChild("br");
      content.addChild("#", errbuf.toString());
    }

    HTMLNode content =
        ctx.getPageMaker()
            .getInfobox(
                "infobox-normal",
                l10n("possibilitiesTitle"),
                contentNode,
                "configuration-possibilities",
                false);
    content.addChild(
        "a",
        new String[] {"href", "title"},
        new String[] {path(), l10n("shortTitle")},
        l10n("returnToNodeConfig"));
    content.addChild("br");
    addHomepageLink(content);

    writeHTMLReply(ctx, 200, "OK", pageNode.generate());
  }