SoftInstigate / restheart

Rapid API Development with MongoDB
https://restheart.org
GNU Affero General Public License v3.0
807 stars 171 forks source link

Unable to read form data in plugin #430

Closed austinrappa78 closed 2 years ago

austinrappa78 commented 2 years ago

When creating a plugin to read form data, this error is thrown:

java.lang.IllegalStateException: UT000005: getRequestChannel() has already been called

Expected Behavior

Read form data.

Current Behavior

12:59:32.194 [main] ←[34mINFO ←[0;39m org.restheart.Bootstrapper - Starting ←[31;1mRESTHeart←[m instance ←[31;1mdefault←[m
 12:59:32.195 [main] ←[34mINFO ←[0;39m org.restheart.Bootstrapper - Version 6.2.2
 12:59:32.220 [main] ←[34mINFO ←[0;39m org.restheart.Bootstrapper - Logging to console with level INFO
 12:59:32.243 [main] ←[34mINFO ←[0;39m org.restheart.plugins.PluginsFactory - Found plugin jar file:/C:/Git/test/test-restheart-v7/restheart/plugins/restheart-graphql.jar
 12:59:32.243 [main] ←[34mINFO ←[0;39m org.restheart.plugins.PluginsFactory - Found plugin jar file:/C:/Git/test/test-restheart-v7/restheart/plugins/restheart-mongodb.jar
 12:59:32.244 [main] ←[34mINFO ←[0;39m org.restheart.plugins.PluginsFactory - Found plugin jar file:/C:/Git/test/test-restheart-v7/restheart/plugins/restheart-polyglot.jar
 12:59:32.244 [main] ←[34mINFO ←[0;39m org.restheart.plugins.PluginsFactory - Found plugin jar file:/C:/Git/test/test-restheart-v7/restheart/plugins/restheart-security.jar
 12:59:32.251 [main] ←[34mINFO ←[0;39m org.restheart.plugins.PluginsFactory - Found plugin jar file:/C:/Git/test/test-restheart-v7/restheart/plugins/test-restheart-v7.jar
 12:59:33.043 [main] ←[31mWARN ←[0;39m o.r.polyglot.PolyglotDeployer - Not running on GraalVM, polyglot plugins deployer disabled!
 12:59:33.053 [main] ←[34mINFO ←[0;39m o.r.mongodb.db.MongoClientSingleton - Connecting to MongoDB...
 12:59:33.144 [main] ←[34mINFO ←[0;39m o.r.mongodb.db.MongoClientSingleton - MongoDB version ←[35m5.0.6←[m
 12:59:33.152 [main] ←[31mWARN ←[0;39m o.r.mongodb.db.MongoClientSingleton - MongoDB is a standalone instance.
 12:59:33.242 [main] ←[34mINFO ←[0;39m org.restheart.mongodb.MongoService - ←[32mURI /api bound to MongoDB resource /yardSale←[m
 12:59:33.269 [main] ←[34mINFO ←[0;39m org.restheart.Bootstrapper - HTTP listener bound at 0.0.0.0:8080
 12:59:33.277 [main] ←[34mINFO ←[0;39m org.restheart.Bootstrapper - ←[32mURI / bound to service mongo, secured: true, uri match PREFIX←[m
 12:59:33.278 [main] ←[34mINFO ←[0;39m org.restheart.Bootstrapper - ←[32mURI /resetpassword/confirmed bound to service resetpasswordConfirmed, secured: false, uri match PREFIX←[m
 12:59:33.278 [main] ←[34mINFO ←[0;39m org.restheart.Bootstrapper - ←[32mURI /resetpassword bound to service resetpassword, secured: false, uri match PREFIX←[m
 12:59:33.279 [main] ←[34mINFO ←[0;39m org.restheart.Bootstrapper - ←[32mURI /graphql bound to service graphql, secured: true, uri match PREFIX←[m
 12:59:33.279 [main] ←[34mINFO ←[0;39m org.restheart.Bootstrapper - ←[32mURI /ic bound to service cacheInvalidator, secured: false, uri match PREFIX←[m
 12:59:33.279 [main] ←[34mINFO ←[0;39m org.restheart.Bootstrapper - ←[32mURI /csv bound to service csvLoader, secured: true, uri match PREFIX←[m
 12:59:33.280 [main] ←[34mINFO ←[0;39m org.restheart.Bootstrapper - ←[32mURI /roles bound to service roles, secured: false, uri match PREFIX←[m
 12:59:33.281 [main] ←[34mINFO ←[0;39m org.restheart.Bootstrapper - ←[32mURI /tokens bound to service rndTokenService, secured: false, uri match PREFIX←[m
 12:59:33.281 [main] ←[34mINFO ←[0;39m org.restheart.Bootstrapper - ←[32mURI /ping bound to service ping, secured: false, uri match PREFIX←[m
 12:59:33.285 [main] ←[34mINFO ←[0;39m org.restheart.Bootstrapper - ←[32mURI / bound to static resource C:\Git\test\test-restheart\www←[m
 12:59:33.414 [main] ←[1;31mERROR←[0;39m o.r.m.h.c.ChangeStreamsActivator - Cannot enable Change Streams: MongoDB is a standalone instance and Change Streams require a Replica Set.
 12:59:33.415 [main] ←[1;31mERROR←[0;39m o.r.m.h.sessions.TxnsActivator - Cannot enable Transactions: MongoDB is a standalone instance and Transactions require a Replica Set.
 12:59:33.415 [main] ←[34mINFO ←[0;39m org.restheart.Bootstrapper - ←[32;1mRESTHeart started←[m
 12:59:38.389 [XNIO-1 task-1] ←[34mINFO ←[0;39m org.restheart.handlers.RequestLogger - GET http://localhost:8080/resetpassword?id=62227d2ad33c1c1248b2eec8 from /[0:0:0:0:0:0:0:1]:59970 => status=←[32;1m200←[m elapsed=68ms contentLength=8120
 12:59:41.270 [XNIO-1 task-1] ←[1;31mERROR←[0;39m c.r.y.r.p.ResetPasswordConfirmPlugin - com.test.restheart.plugins.ResetPasswordConfirmPlugin.POST.error.log
 12:59:41.270 [XNIO-1 task-1] ←[1;31mERROR←[0;39m c.r.y.r.p.ResetPasswordConfirmPlugin - java.lang.IllegalStateException: UT000005: getRequestChannel() has already been called
 12:59:41.271 [XNIO-1 task-1] ←[1;31mERROR←[0;39m c.r.y.r.p.ResetPasswordConfirmPlugin - error
 java.io.IOException: java.lang.IllegalStateException: UT000005: getRequestChannel() has already been called
        at io.undertow.server.handlers.form.FormEncodedDataDefinition$FormEncodedDataParser.parseBlocking(FormEncodedDataDefinition.java:291)
Caused by: java.lang.IllegalStateException: UT000005: getRequestChannel() has already been called
        at io.undertow.server.handlers.form.FormEncodedDataDefinition$FormEncodedDataParser.parseBlocking(FormEncodedDataDefinition.java:291)
com.test.restheart.plugins.ResetPasswordConfirmPlugin.POST.error.out
java.lang.IllegalStateException: UT000005: getRequestChannel() has already been called
java.io.IOException: java.lang.IllegalStateException: UT000005: getRequestChannel() has already been called
        at io.undertow.server.handlers.form.FormEncodedDataDefinition$FormEncodedDataParser.parseBlocking(FormEncodedDataDefinition.java:291)
        at com.test.restheart.plugins.ResetPasswordConfirmPlugin.post(ResetPasswordConfirmPlugin.java:120)
        at com.test.restheart.plugins.ResetPasswordConfirmPlugin.handle(ResetPasswordConfirmPlugin.java:62)
        at com.test.restheart.plugins.ResetPasswordConfirmPlugin.handle(ResetPasswordConfirmPlugin.java:35)
        at org.restheart.handlers.ServiceWrapper.handleRequest(PipelinedWrappingHandler.java:139)
        at org.restheart.handlers.PipelinedWrappingHandler.handleRequest(PipelinedWrappingHandler.java:121)
        at io.undertow.server.handlers.encoding.EncodingHandler.handleRequest(EncodingHandler.java:72)
        at org.restheart.handlers.ConfigurableEncodingHandler.handleRequest(ConfigurableEncodingHandler.java:81)
        at org.restheart.handlers.PipelinedWrappingHandler.handleRequest(PipelinedWrappingHandler.java:121)
        at org.restheart.handlers.PipelinedHandler.next(PipelinedHandler.java:78)
        at org.restheart.handlers.QueryStringRebuilder.handleRequest(QueryStringRebuilder.java:93)
        at org.restheart.handlers.PipelinedHandler.next(PipelinedHandler.java:78)
        at org.restheart.handlers.RequestInterceptorsExecutor.handleRequest(RequestInterceptorsExecutor.java:145)
        at org.restheart.handlers.PipelinedHandler.next(PipelinedHandler.java:78)
        at org.restheart.security.handlers.AuthorizersHandler.handleRequest(AuthorizersHandler.java:65)
        at org.restheart.handlers.PipelinedHandler.next(PipelinedHandler.java:78)
        at org.restheart.handlers.injectors.TokenInjector.handleRequest(TokenInjector.java:62)
        at org.restheart.handlers.PipelinedHandler.next(PipelinedHandler.java:78)
        at org.restheart.security.handlers.AuthenticationCallHandler.handleRequest(AuthenticationCallHandler.java:68)
        at org.restheart.handlers.PipelinedHandler.next(PipelinedHandler.java:78)
        at org.restheart.security.handlers.AuthenticationConstraintHandler.handleRequest(AuthenticationConstraintHandler.java:80)
        at org.restheart.handlers.PipelinedHandler.next(PipelinedHandler.java:78)
        at org.restheart.security.handlers.AuthenticatorMechanismsHandler.handleRequest(AuthenticatorMechanismsHandler.java:60)
        at org.restheart.handlers.PipelinedHandler.next(PipelinedHandler.java:78)
        at org.restheart.security.handlers.SecurityInitialHandler.handleRequest(SecurityInitialHandler.java:78)
        at org.restheart.handlers.PipelinedHandler.next(PipelinedHandler.java:78)
        at org.restheart.security.handlers.SecurityHandler.handleRequest(SecurityHandler.java:92)
        at org.restheart.handlers.PipelinedHandler.next(PipelinedHandler.java:78)
        at org.restheart.handlers.QueryStringRebuilder.handleRequest(QueryStringRebuilder.java:93)
        at org.restheart.handlers.PipelinedHandler.next(PipelinedHandler.java:78)
        at org.restheart.handlers.RequestInterceptorsExecutor.handleRequest(RequestInterceptorsExecutor.java:145)
        at org.restheart.handlers.PipelinedHandler.next(PipelinedHandler.java:78)
        at org.restheart.handlers.injectors.XPoweredByInjector.handleRequest(XPoweredByInjector.java:62)
        at org.restheart.handlers.PipelinedHandler.next(PipelinedHandler.java:78)
        at org.restheart.handlers.CORSHandler.handleRequest(CORSHandler.java:103)
        at org.restheart.handlers.PipelinedHandler.next(PipelinedHandler.java:78)
        at org.restheart.handlers.ServiceExchangeInitializer.handleRequest(ServiceExchangeInitializer.java:80)
        at org.restheart.handlers.PipelinedHandler.next(PipelinedHandler.java:78)
        at org.restheart.handlers.RequestLogger.handleRequest(RequestLogger.java:86)
        at org.restheart.handlers.PipelinedHandler.next(PipelinedHandler.java:78)
        at org.restheart.handlers.TracingInstrumentationHandler.handleRequest(TracingInstrumentationHandler.java:54)
        at org.restheart.handlers.PipelinedHandler.next(PipelinedHandler.java:78)
        at org.restheart.handlers.injectors.PipelineInfoInjector.handleRequest(PipelineInfoInjector.java:65)
        at io.undertow.server.handlers.PathHandler.handleRequest(PathHandler.java:104)
        at io.undertow.server.handlers.HttpContinueAcceptingHandler.handleRequest(HttpContinueAcceptingHandler.java:83)
        at org.restheart.handlers.ErrorHandler.handleRequest(ErrorHandler.java:61)
        at io.undertow.server.Connectors.executeRootHandler(Connectors.java:387)
        at io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:852)
        at org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
        at org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:2019)
        at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1558)
        at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1449)
        at org.xnio.XnioWorker$WorkerThreadFactory$1$1.run(XnioWorker.java:1280)
        at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: java.lang.IllegalStateException: UT000005: getRequestChannel() has already been called
        ... 54 more
12:59:41.286 [XNIO-1 task-1] ←[34mINFO ←[0;39m org.restheart.handlers.RequestLogger - POST http://localhost:8080/resetpassword/confirmed from /[0:0:0:0:0:0:0:1]:59970 => status=←[31;1m500←[m elapsed=23ms contentLength=116

Context

Upgrading from v3 to v6

Environment

    <dependency>
        <groupId>org.restheart</groupId>
        <artifactId>restheart-commons</artifactId>
        <version>6.2.2</version>
    </dependency>

Steps to Reproduce

Create html

        <div class="card">
            <h2>Reset your password</h2>
            <form action="/resetpassword/confirmed" method="post">
            <input class="password" type="password" name="password" required>
            <input type="hidden" name="id" value="id">
            <input class="button" type="submit" value="Submit">
            </form>
        </div>

Create plugin

import org.bson.Document;
import org.restheart.exchange.ByteArrayRequest;
import org.restheart.exchange.ByteArrayResponse;
import org.restheart.plugins.ByteArrayService;
import org.restheart.plugins.InjectMongoClient;
import org.restheart.plugins.RegisterPlugin;
import org.restheart.utils.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.mongodb.client.MongoClient;

import io.undertow.server.handlers.form.FormData;
import io.undertow.server.handlers.form.FormData.FormValue;
import io.undertow.server.handlers.form.FormDataParser;
import io.undertow.server.handlers.form.FormParserFactory;

/**
 * Reset user password
 */
@RegisterPlugin(
        name = "resetpasswordConfirmed",
        description = "Confirm reset user password",
        enabledByDefault = true,
        defaultURI = "/resetpassword/confirmed")
public class ResetPasswordConfirmPlugin implements ByteArrayService {

        private static final Logger log = LoggerFactory.getLogger(ResetPasswordConfirmPlugin.class);
        private ResetPasswordRepository repository;

        @InjectMongoClient
        public void init(MongoClient mongoClient) {
            repository = new ResetPasswordRepository(mongoClient);
        }

        /**
         * Handle request and response
         */
        @Override
        public void handle(ByteArrayRequest request, ByteArrayResponse response) throws Exception {
            switch (request.getMethod()) {
                case OPTIONS:
                    this.options(request, response);
                    break;
                case POST:
                    this.post(request, response);
                    break;
                default:
                    this.notAllowed(request, response);
                    break;
            }
        }

        /**
         * Post method
         * @param request
         * @param response
         */
        private void post(ByteArrayRequest request, ByteArrayResponse response) {
            try {     
                FormParserFactory builder = FormParserFactory.builder().build();
                FormDataParser parser = builder.createParser(request.getExchange());
                if (parser == null) {
                    throw new Exception("Not a form.");       
                }

                FormData formData = parser.parseBlocking();

                //Get id attribute
                FormValue id = null;
                if (formData.contains("id"))
                    id = formData.get("id").pop();

                if (id == null) {
                    throw new Exception("Missing id parameter.");    
                }

                //Get password attribute
                FormValue password = null;
                if (formData.contains("password"))
                    password = formData.get("password").pop();

                if (password == null) {
                    throw new Exception("Missing information.");       
                }     

                //Get the user password reset document
                Document document = repository.findUserPasswordReset(id.getValue());
                if (document == null) {
                    throw new Exception("Password reset expired. Please return to the app and reset your password again.");
                }    

                EncryptionRepository encryption = new EncryptionRepository();
                String encryptedPassword = encryption.encrypt(password.getValue());

                repository.updateUser(document.getObjectId("userId").toString(), encryptedPassword);

                //Set user password reset document to inactive
                repository.deleteUserPasswordReset(id.getValue());

                //Build html form
                StringBuilder message = new StringBuilder();            
                message.append("<div>Your password has been reset. Please return to the app.</div>");            

                String html = displayMessage(message.toString());            

                response.setStatusCode(HttpStatus.SC_OK);
                response.setContentType("text/html; charset=utf-8");
                response.setContent(html);
            }    
            catch (Exception ex) {
                log.error(this.getClass().getName() + ".POST.error.log");
                log.error(ex.getMessage());
                log.error("error", ex);
                System.out.println(this.getClass().getName() + ".POST.error.out");
                System.out.println(ex.getMessage());

                String errorMessage = displayMessage(ex.getMessage());
                ex.printStackTrace();

                response.setStatusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR);
                response.setContentType("application/json; charset=utf-8");
                response.setContent(errorMessage);
            }        
        }    

        /**
         * Options method
         * @param request
         * @param response
         */
        private void options(ByteArrayRequest request, ByteArrayResponse response) {
            handleOptions(request);
        }

        /**
         * Method not allowed
         * @param request
         * @param response
         */
        private void notAllowed(ByteArrayRequest request, ByteArrayResponse response) {
            response.setStatusCode(HttpStatus.SC_METHOD_NOT_ALLOWED);
        }

        /**
         * Builds json message
         */
        private String displayMessage(String message) {
            return "{ \"error\": \"" + message + "\" }";
        }
}

Possible Implementation

Please include an example of a FormData plugin in your plugin-examples project.

austinrappa78 commented 2 years ago

This appears to be a reversion from https://github.com/SoftInstigate/restheart/issues/299

ujibang commented 2 years ago

In order to use undertow's FormParser you need a custom Request.

This is needed because ByteArrayRequest (like any other Request implementation) consumes the request body; that's why you get the error getRequestChannel() has already been called from FormParser.

I created an example with a possibile implementation at https://github.com/SoftInstigate/restheart-examples/tree/master/form-handler

austinrappa78 commented 2 years ago

Confirmed it worked and can accept FormData.