/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ratpack.session.clientside;
import com.google.inject.Provides;
import com.google.inject.Scopes;
import com.google.inject.Singleton;
import io.netty.util.CharsetUtil;
import ratpack.guice.ConfigurableModule;
import ratpack.session.SessionStore;
import ratpack.session.clientside.internal.*;
import javax.crypto.spec.SecretKeySpec;
/**
* An extension module that provides a client side session store - cookie based.
* <p>
* This module depends on {@link ratpack.session.SessionModule} and <b>MUST</b> be added to the module list <b>AFTER</b> {@link ratpack.session.SessionModule}.
*
* <h3>Example usage</h3>
* <pre class="java">{@code
* import ratpack.guice.Guice;
* import ratpack.http.client.ReceivedResponse;
* import ratpack.session.Session;
* import ratpack.session.SessionKey;
* import ratpack.session.SessionModule;
* import ratpack.session.clientside.ClientSideSessionModule;
* import ratpack.test.embed.EmbeddedApp;
*
* import static org.junit.Assert.*;
*
* public class ClientSideSessionModuleExample {
* public static void main(String... args) throws Exception {
* EmbeddedApp.of(s -> s
* .registry(Guice.registry(b -> b
* .module(SessionModule.class, config -> {
* //config.path("/").domain("www.example.com");
* })
* .module(ClientSideSessionModule.class, config -> {
* config.setSessionCookieName("session_name");
* config.setSecretToken("your token for signing");
* //config.setSecretKey("key for cipher");
* //config.setMacAlgorithm("MAC algorithm for signing");
* //config.setCipherAlgorithm("Cipher Algorithm");
* //config.setMaxSessionCookieSize(1024);
* //config.setMaxInactivityInterval(Duration.ofSeconds(60));
* })
* ))
* .handlers(chain -> chain
* .get(ctx -> {
* ctx.get(Session.class).getData()
* .map(d -> d.get(SessionKey.ofType("value", "value")).orElse("not set"))
* .then(ctx::render);
* })
* .get("set/:value", ctx -> {
* ctx.get(Session.class).getData().then(d -> {
* d.set("value", ctx.getPathTokens().get("value"));
* ctx.render(d.get(SessionKey.ofType("value", "value")).orElse("not set"));
* });
* })
* )
* )
* .test(client -> {
* ReceivedResponse response = client.get();
* assertEquals("not set", response.getBody().getText());
* assertFalse("No cookies should be set", response.getHeaders().getAll("Set-Cookie").contains("session_name"));
*
* response = client.get("set/foo");
* assertEquals("foo", response.getBody().getText());
* assertTrue("We set a value and our session name", response.getHeaders().getAll("Set-Cookie")
* .stream()
* .anyMatch(c -> c.startsWith("session_name")));
*
* response = client.get();
* assertEquals("foo", response.getBody().getText());
* assertFalse("We did not update session", response.getHeaders().getAll("Set-Cookie")
* .stream()
* .anyMatch(c -> c.startsWith("session_name")));
* });
* }
* }
* }</pre>
*
* <h3>Notes</h3>
* <p>
* The max cookie size for a client is 4k so it's important to keep
* this in mind when using the {@link ClientSideSessionModule}
*
* <p>
* By default your session will be signed but not encrypted. This is because the <strong>secretKey</strong>
* is not set by default. That is, your users will not be able to tamper with the
* cookie but they can still read the key value pairs that you have set. If you want to render
* the entire cookie unreadable make sure you set a <strong>secretKey</strong>
*
* <p>
* When setting your own <strong>secretKey</strong> and <strong>cipherAlgorithm</strong>
* make sure that the key length is acceptable according to the algorithm you have chosen.
*
* <p>
* When working in multi instances environment the
* {@code secretToken} has to be the same for every ratpack instance configuration.
*/
public class ClientSideSessionModule extends ConfigurableModule<ClientSideSessionConfig> {
@Override
protected void configure() {
bind(SessionStore.class).to(ClientSideSessionStore.class).in(Scopes.SINGLETON);
}
@Provides
@Singleton
Signer signer(ClientSideSessionConfig config) {
byte[] token = config.getSecretToken().getBytes(CharsetUtil.UTF_8);
return new DefaultSigner(new SecretKeySpec(token, config.getMacAlgorithm()));
}
@Provides
@Singleton
Crypto crypto(ClientSideSessionConfig config) {
if (config.getSecretKey() == null || config.getCipherAlgorithm() == null) {
return NoCrypto.INSTANCE;
} else {
return new DefaultCrypto(config.getSecretKey().getBytes(CharsetUtil.UTF_8), config.getCipherAlgorithm());
}
}
}