I need to write a web client that hits a legacy web app, logs in to it, pulls down some information from a /widget page, and does some work based off of this page's HTML. I'm choosing to go with a Groovy/HttpBuilder solution, for reasons outside the scope of this question.
The only shortcoming (from what I can tell) is that HttpBuilder doesn't support the retaining of cookies between requests. This is a major problem since the (Java) web app uses JSESSIONID cookies to determine whether or not the user is logged in, has permissions, etc.
So first, if my assertion above is incorrect, and HttpBuilder does support the retaining of cookies across requests, please correct me and perhaps the answer here is a solution that shows me how to tap into this part of HttpBuilder. In this case all of my code below is moot.
Assuming I'm correct and this isn't handled by HttpBuilder, I found this excellent solution that I can't get to work for some reason, hence my question.
My adaptation of that code (see link above) is as follows:
TaskAutomator.groovy
====================
package com.me.myapp.tasker
import groovyx.net.http.ContentType
import groovyx.net.http.Method
class TaskAutomator {
static void main(String[] args) {
TaskAutomator tasker = new TaskAutomator()
String result = tasker.doWork("http://myapp.example.com")
println result
}
String doWork(String baseUrl) {
CookieRetainingHttpBuilder cookiedBuilder = new CookieRetainingHttpBuilder(baseUrl)
Map logins = [username: 'user', password: '12345']
// Go to the main page where we will get back the HTML for a login screen.
// We don't really care about the response here, so long as its HTTP 200.
cookiedBuilder.request(Method.GET, ContentType.HTML, "", null)
// Log in to the app, where, on success, we will get back the HTML for a the
// "Main Menu" screen users see when they log in. We don't really care about
// the response here, so long as its HTTP 200.
cookiedBuilder.request(Method.POST, ContentType.HTML, "/auth", logins)
// Finally, now that our JSESSIONID cookies is authenticated, go to the widget page
// which is what we actually care about interacting with.
def response = cookiedBuilder.request(Method.GET, ContentType.HTML, "/widget", null)
// Test to make sure the response is what I think it is.
print response
String result
// TODO: Now actually do work based off the response.
result
}
}
CookieRetainingHttpBuilder
==========================
package com.me.myapp.tasker
import groovyx.net.http.ContentType
import groovyx.net.http.HTTPBuilder
import groovyx.net.http.HttpResponseDecorator
import groovyx.net.http.Method
class CookieRetainingHttpBuilder {
private String baseUrl
private HTTPBuilder httpBuilder
private List<String> cookies
CookieRetainingHttpBuilder(String baseUrl) {
this.baseUrl = baseUrl
this.httpBuilder = initializeHttpBuilder()
this.cookies = []
}
public def request(Method method, ContentType contentType, String url, Map<String, Serializable> params) {
httpBuilder.request(method, contentType) { request ->
uri.path = url
uri.query = params
headers['Cookie'] = cookies.join(';')
}
}
private HTTPBuilder initializeHttpBuilder() {
def httpBuilder = new HTTPBuilder(baseUrl)
httpBuilder.handler.success = { HttpResponseDecorator resp, reader ->
resp.getHeaders('Set-Cookie').each {
String cookie = it.value.split(';')[0]
cookies.add(cookie)
}
reader
}
httpBuilder
}
}
When I run this code I get the following stack trace (I've culled out un-interesting parts as its pretty large):
Exception in thread "main" groovyx.net.http.HttpResponseException: Not Found
at groovyx.net.http.HTTPBuilder.defaultFailureHandler(HTTPBuilder.java:642)
... (lines omitted for brevity)
at groovyx.net.http.HTTPBuilder$1.handleResponse(HTTPBuilder.java:494)
... (lines omitted for brevity)
at groovyx.net.http.HTTPBuilder.doRequest(HTTPBuilder.java:506)
at groovyx.net.http.HTTPBuilder.doRequest(HTTPBuilder.java:425)
at groovyx.net.http.HTTPBuilder.request(HTTPBuilder.java:374)
at groovyx.net.http.HTTPBuilder$request.call(Unknown Source)
at com.me.myapp.tasker.CookieRetainingHttpBuilder.request(CookieRetainingHttpBuilder.groovy:20)
... (lines omitted for brevity)
at com.me.myapp.tasker.TaskAutomator.doWork(TaskAutomator.groovy:23)
... (lines omitted for brevity)
at com.me.myapp.tasker.TaskAutomator.main(TaskAutomator.groovy:13)
CookieRetainingHttpBuilder:20 is this line from request:
httpBuilder.request(method, contentType) { request ->
Can anyone see why I'm getting this? Additionally, I wanted to confirm my approach/strategy in the TaskAutomater#doWork(...) method. Is my use of CookieRetainingHttpBuilder "correct" in the sense that I'm:
Going to the main/login page
POSTing login creds and logging in
Going to the widget page
Or is there a different way to use HttpBuilder that is better/more efficient here (remember CookieRetainingHttpBuilder is, after all, just a wrapper for HttpBuilder).
I believe that error may be manifesting due to missing imports, or perhaps an older version of HttpBuilder. Looking into HttpBuilder.Class, I see this, which informs my suggestions:
protected java.lang.Object parseResponse(org.apache.http.HttpResponse resp, java.lang.Object contentType) throws groovyx.net.http.HttpResponseException { /* compiled code */ }
I am fairly certain you can just use headers.'Set-Cookiein your httpBuilder setup. The syntax is different from what you have, but the change is small and simple, and this is the base method I use when using HttpBuilder.
#Grab(group = 'org.codehaus.groovy.modules.http-builder', module = 'http-builder', version = '0.7)
import groovyx.net.http.HTTPBuilder
import org.apache.http.HttpException
import static groovyx.net.http.ContentType.TEXT
import static groovyx.net.http.Method.GET
def http = new HTTPBuilder(urlToHit)
http.request(urlToHit, GET, TEXT) { req ->
headers.'User-Agent' = ${userAgent}
headers.'Set-Cookie' = "${myCookie}"
response.success = { resp, reader ->
html = reader.getText()
}
response.failure = { resp, reader ->
System.err.println "Failure response: ${resp.status}"
throw new HttpException()
}
}
Something else to note is that you have no failure handling. I don't know if that will raise an exception, but it could be worth looking into.
EDIT
As suggested, I'm merging my answers (thanks for letting me know...I wasn't sure what proper etiquette was).
Here's what I've come up with. I did my best to reuse the code you posted. I commented as best I could. If you have any questions, let me know.
#Grab(group = 'org.codehaus.groovy.modules.http-builder', module = 'http-builder', version = '0.7')
import static groovyx.net.http.ContentType.HTML
import static groovyx.net.http.Method.POST
import static groovyx.net.http.Method.GET
import groovyx.net.http.ContentType
import groovyx.net.http.HTTPBuilder
import groovyx.net.http.URIBuilder
import groovyx.net.http.Method
import org.apache.http.HttpException
/**
* This class defines the methods used for getting and using cookies
* #param baseUrl The URL we will use to make HTTP requests. In this example, it is https://www.pinterest.com
*/
class CookieRetainingHttpBuilder {
String baseUrl
/**
* This method makes an http request and adds cookies to the array list for later use
* #param method The method used to make the http request. In this example, we use GET and POST
* #param contentType The content type we are requesting. In this example, we are getting HTML
* #param url The URI path for the appropriate page. For example, /login/ is for the login page
* #param params The URI query used for setting parameters. In this example, we are using login credentials
*/
public request (Method method, ContentType contentType, String url, Map<String, Serializable> params) {
List<String> cookies = new ArrayList<>()
def http = new HTTPBuilder(baseUrl)
http.request(baseUrl, method, contentType) { req ->
URIBuilder uriBuilder = new URIBuilder(baseUrl)
uriBuilder.query = params
uriBuilder.path = url
headers.'Accept' = HTML
headers.'User-Agent' = "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36"
headers.'Set-Cookie' = cookies.join(";")
response.success = { resp, reader ->
resp.getHeaders('Set-Cookie').each {
def cookie = it.value.split(";").toString()
cookies.add(cookie)
}
return reader
}
response.failure = { resp, reader ->
System.err.println "Failure response: ${resp.status}"
throw new HttpException()
}
}
}
}
/**
* This class contains the method to make HTTP requests in the proper sequence
* #param base The base URL
* #param user The username of the site being logged in to
* #param pass The password for the username
*/
class TaskAutomator {
private static String base = "http://myapp.example.com"
private static String user = "thisIsMyUser"
private static String pass = "thisIsMyPassword"
/**
* This method contains the functions in proper order to set cookies and login to a site
* #return response Returns the HTML from the final GET request
*/
static String doWork () {
CookieHandler.setDefault(new CookieManager());
CookieRetainingHttpBuilder cookiedBuilder = new CookieRetainingHttpBuilder(baseUrl: base)
Map logins = [username: user, password: pass]
// Go to the main page where we will get back the HTML for a login screen.
// We don't really care about the response here, so long as its HTTP 200.
cookiedBuilder.request(GET, HTML, "", null)
// Log in to the app, where, on success, we will get back the HTML for a the
// "Main Menu" screen users see when they log in. We don't really care about
// the response here, so long as its HTTP 200.
cookiedBuilder.request(POST, HTML, "/login/", logins)
// Finally, now that our JSESSIONID cookies is authenticated, go to the widget page
// which is what we actually care about interacting with.
def response = cookiedBuilder.request(GET, HTML, "/", null)
// Test to make sure the response is what I think it is.
return response
// TODO: Now actually do work based off the response.
}
}
TaskAutomator tasker = new TaskAutomator()
String result = tasker.doWork()
println result
Related
I'm trying to use Digest authentication with HTTP Client against a 3rd-party web service that I don't control.
I started out with the sample code from here:
http://hc.apache.org/httpcomponents-client-4.5.x/httpclient/examples/org/apache/http/examples/client/ClientPreemptiveDigestAuthentication.java
I got it working against httpbin.org, before attempting the next step described below.
It appears that the target 3rd-party service that I'm using requires the opaque value to be copied from the WWW-Authentication header on the initial response to the Authorization header on the next request, as described here:
https://security.stackexchange.com/questions/24425/what-is-the-opaque-field-in-http-digest-access-authentication-used-for
However, I have turned on wire-logging and stepped through the code (again this is really just the sample code linked above, no need to copy/paste it here) and I see that the opaque is NOT copied.
Any ideas what prevents it from being copied?
I even tried overriding the processChallenge method:
DigestScheme digestAuth = new DigestScheme() {
#Override
public void processChallenge(
Header header) throws MalformedChallengeException {
but it appears that any value introduced into the Parameters at this point is ignored in the next request.
Finally fixed by overriding the Authorize header explicitly, instead of relying on the internals of HttpClient to do it automatically:
package [...];
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.commons.io.IOUtils;
import org.apache.http.*;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.*;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.impl.auth.DigestScheme;
import org.apache.http.impl.client.*;
import org.testng.Assert;
public class DigestTest {
private static final String URL
= "https://...";
private static final String PASSWORD = ...;
private static final String USER = ...;
public static void main(String[] args) throws Exception {
new DigestTest().run();
}
public void run() throws Exception {
HttpGet httpget = new HttpGet(URL);
HttpHost target
= new HttpHost(httpget.getURI().getHost(), 443, "https");
CredentialsProvider credsProvider = new BasicCredentialsProvider();
UsernamePasswordCredentials credentials
= new UsernamePasswordCredentials(USER, PASSWORD);
credsProvider.setCredentials(
new AuthScope(target.getHostName(), target.getPort()),
credentials);
CookieStore cookieStore = new BasicCookieStore();
CloseableHttpClient httpclient
= HttpClients.custom().setDefaultCookieStore(cookieStore)
.setDefaultCredentialsProvider(credsProvider).build();
try {
DigestScheme digestAuth = new DigestScheme();
digestAuth.overrideParamter("qop", "auth");
digestAuth.overrideParamter("nc", "0");
digestAuth.overrideParamter("cnonce", DigestScheme.createCnonce());
AuthCache authCache = new BasicAuthCache();
authCache.put(target, digestAuth);
HttpClientContext localContext = HttpClientContext.create();
localContext.setAuthCache(authCache);
CloseableHttpResponse response;
response = httpclient.execute(target, httpget, localContext);
Map<String, String> wwwAuth = Arrays
.stream(response.getHeaders("WWW-Authenticate")[0]
.getElements())
.collect(Collectors.toMap(HeaderElement::getName,
HeaderElement::getValue));
// the first call ALWAYS fails with a 401
Assert.assertEquals(response.getStatusLine().getStatusCode(), 401);
digestAuth.overrideParamter("opaque", wwwAuth.get("opaque"));
digestAuth.overrideParamter("nonce", wwwAuth.get("nonce"));
digestAuth.overrideParamter("realm", wwwAuth.get("Digest realm"));
Header authenticate = digestAuth.authenticate(credentials, httpget,
localContext);
httpget.addHeader(authenticate);
response = httpclient.execute(target, httpget, localContext);
// the 2nd call is the real deal
Assert.assertEquals(response.getStatusLine().getStatusCode(), 200);
System.out.println(IOUtils
.toString(response.getEntity().getContent(), "utf-8"));
} finally {
httpclient.close();
}
}
}
This is an attempt to use Loopj for a Synchronous put and post call from a HTTP utility class. The code uses a syncrhonous client as it’s used inside an AsyncTask and some UI interactions depend heavily on the json response so the AsyncTask is managing making the call asynchronously.
All the get calls from the HTTP utility class are working successfully. The post and put do not and they both appear to have exactly the same problem.
The json string is created using Gson. I’ve tested the json output from the application directly in Postman and it posts exactly as expected to the API, so it appears to be well formed and behaves totally as expected without any errors.
Both the put and post calls are constructed without throwing an error. Basic authorization is being added (as shown on the client instance). The SyncHTTPClient put method is called using a null context parameter. I did a bit of research and found a single post where this was being done successfully.
https://github.com/loopj/android-async-http/issues/1139
The put call fires but doesn’t enter either the overridden methods of the handler. It just returns null. I've included a portion of the working class to view:
public void executePutSave(String name, String pass, String jsonBody) {
client.setBasicAuth(name, pass);
executeLoopJPutCall("/api/Save", jsonBody);
}
public void executeLoopJPutCall(String relativeUrl, String jsonBody) {
String url = getAbsoluteUrl(relativeUrl);
StringEntity entity = new StringEntity(jsonBody, "UTF-8");
jsonResponse = null;
client.put(null, url, entity, "application/json", new JsonHttpResponseHandler() {
#Override
public void onSuccess(int statusCode, Header[] headers, JSONObject response) {
super.onSuccess(statusCode, headers, response);
jsonResponse = response.toString();
Log.i(TAG, "onSuccess: " + jsonResponse);
}
#Override
public void onFailure(int statusCode, Header[] headers, Throwable throwable, JSONObject errorResponse) {
super.onFailure(statusCode, headers, throwable, errorResponse);
jsonResponse = errorResponse.toString();
Log.e(TAG, "onFailure: " + statusCode + errorResponse );
}
}
);
}
So, apparently the header must be added explicitly when using the above code to Post or Put json to the API. Once I changed the header authentication line from this:
client.setBasicAuth(name, pass);
To this:
String userpass = name + ":" + pass;
String encoded = new String(Base64.encode(userpass.getBytes(),Base64.NO_WRAP));
client.addHeader("Authorization", "Basic "+encoded);
...everything worked as expected.
I found the information on this blog: https://github.com/loopj/android-async-http/issues/113
Passing a null context worked, too.
I'm using Camel 2.15.3 and camel-netty4, and since upgrading from camel-netty3, I'm having problems receiving full JSON messages via UDP. Each JSON message is about 3 to 5 kbytes, but my MessageToMessageDecoder implementation is only giving me the first 2048 (i.e. 2k bytes). From a test program, I send in one UDP message, and from my debug prints within my MessageToMessageDecoder it shows that the decode() method is only called once.
I'm currently reading through Netty In Action, but i see this in my log file: UnpooledUnsafeDirectByteBuf(ridx: 0, widx: 2048, cap: 2048))
I desperately need to get this fixed in production, and just need to be able to receive JSON messasges via UDP and send them through my Camel routes. I'm confused about what is the best framing (if any) to use?
With netty3 this was working fine and I had a UdpPacketDecoder implements ChannelUpstreamHandler that invoked Channels.fireMessageReceived(ctx, message, me.getRemoteAddress()) to fire the message to the next handler and it seemed to work fine.
My route looks like the below. It consumes from netty4:udp and produces to a SEDA queue, just for now while testing:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<route startupOrder="104" customId="true" id="ROUTE_ID_RAW_CQMS_EVENTS" xmlns="http://camel.apache.org/schema/spring">
<from uri="netty4:udp://devserver-09.dev.s.mission.net:11111?serverPipelineFactory=#CQMS_SERVER_PIPELINE_FACTORY_ROUTE_ID_RAW_CQMS_EVENTS&keepAlive=true&sync=false&receiveBufferSize=26214400&sendBufferSize=26214400&allowDefaultCodec=false&disconnectOnNoReply=false&receiveBufferSizePredictor=8192"/>
<setProperty propertyName="CamelCharsetName" id="setProperty1">
<expressionDefinition>iso-8859-1</expressionDefinition>
</setProperty>
<threads poolSize="7" maxPoolSize="14" threadName="threads_ROUTE_ID_RAW_CQMS_EVENTS" callerRunsWhenRejected="true" id="threads1">
<to uri="seda:SEDA_INPUT_QUEUE_102?size=200000&concurrentConsumers=10&waitForTaskToComplete=Never&failIfNoConsumers=true&timeout=10000" id="to1"/>
<setProperty propertyName="CamelCharsetName" id="setProperty2">
<expressionDefinition>iso-8859-1</expressionDefinition>
</setProperty>
</threads>
</route>
I print out the received DatagramPacket, which shows this: UnpooledUnsafeDirectByteBuf(ridx: 0, widx: 2048, cap: 2048))
Here is my MessageToMessageDecoder implementation:
package com.mission.mplr.multiprotocollistenerrouter;
import com.vonage.mplr.utils.MiscUtils;
import io.netty.channel.ChannelHandlerContext; // Represents the "binding" between a ChannelHandler and the ChannelPipeline.
import io.netty.channel.socket.DatagramPacket;
import io.netty.handler.codec.MessageToMessageDecoder;
import java.nio.charset.Charset;
import java.util.List;
import org.slf4j.Logger; // The org.slf4j.Logger interface is the main user entry point of SLF4J API.
import org.slf4j.LoggerFactory; // Utility class producing Loggers for various logging APIs, most notably for log4j.
public class UdpDatagramDecoder extends MessageToMessageDecoder<DatagramPacket> {
private static final Logger logger = LoggerFactory.getLogger(UdpDatagramDecoder.class);
private static final Logger errorLogger = LoggerFactory.getLogger("ERROR_LOGGER");
private final String CHARSET_NAME;
UdpDatagramDecoder(String charsetName) {
this.CHARSET_NAME = charsetName;
}
#Override
public boolean acceptInboundMessage(Object msg) throws Exception {
return true;
}
#Override
protected void decode(ChannelHandlerContext chc, DatagramPacket packet, List out) throws Exception {
logger.info("decode(): ENTER");
logger.info("decode(): Received datagram = {}", packet);
String packetAsString = packet.content().toString(Charset.forName(CHARSET_NAME));
if(packetAsString == null) {
return; // Nothing to do
} else {
out.add(packetAsString);
packet.retain();
}
logger.info("decode(): bodyBytesAsString[size={}] = {}", packetAsString.length(), packetAsString);
String bodyBytesAsHex = MiscUtils.stringAsHex(packetAsString, CHARSET_NAME);
logger.info("decode(): bodyBytesAsHex[size={}] = {}", bodyBytesAsHex.length(), bodyBytesAsHex);
logger.info("decode(): EXIT");
}
}
// ------------- end --------------
My server pipeline has this initChannel() implementation:
#Override
protected void initChannel(Channel ch) throws Exception {
logger.trace("initChannel(): ENTER");
ChannelPipeline channelPipeline = ch.pipeline();
serverInvoked = true;
String theSourceRouteId = consumer.getRoute().getId();
logger.debug("initChannel(): consumer = {}, theSourceRouteId = {}", consumer.toString(), theSourceRouteId);
// -------------------------------------------------------------------
// Here we add the custom UDP datagram decoder. Decoders are typically
// stateful, thus we create a new instance with every pipeline.
// -------------------------------------------------------------------
String udpPacketDecoderName = "CQMS_UDP_DATAGRAM_DECODER_" + theSourceRouteId;
logger.debug("initChannel(): Adding {}", udpPacketDecoderName);
channelPipeline.addLast(udpPacketDecoderName, new UdpDatagramDecoder(CHARSET_NAME));
// -----------------------------------------------------------------------------------------
// Default Camel ServerChannelHandler for the consumer, to allow Camel to route the message.
// -----------------------------------------------------------------------------------------
String serverChannelHandlerName = "CQMS_SERVER_CHANNEL_HANDLER_" + theSourceRouteId;
logger.debug("initChannel(): Adding {}", serverChannelHandlerName);
channelPipeline.addLast(serverChannelHandlerName, new ServerChannelHandler(consumer));
logger.trace("initChannel(): EXIT");
}
Netty uses 2048 as upper limit for datagram packets by default. You can change this via setting your own instance of FixedRecvByteBufAllocator on the Bootstrap. Not sure how this can be done via Camel though.
Thanks so much Norman! Below is the solution that works for Camel 2.15.3.
Basically, we read the upper limit from the application's configuration and set it within the ServerInitializerFactory's initChannel(Channel ch) method.
#Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline channelPipeline = ch.pipeline();
serverInvoked = true;
// -------------------------------------------------------------------
// Here we add the custom UDP datagram decoder. Decoders are typically
// stateful, thus we create a new instance with every pipeline.
// -------------------------------------------------------------------
String udpDecoderName = "UDP_DECODER_" + theSourceRouteId;
channelPipeline.addLast(udpDecoderName, new UdpPacketDecoder_ADAPTER(CHARSET_NAME));
// ---------------------------------------------------------------------
// Netty4 has default of 2048 bytes as upper limit for datagram packets.
// Here we override the default upper limit based on a config param.
// ---------------------------------------------------------------------
if(ConfigManager.getInstance().getRecvByteBufAllocator() > 0) {
ch.config().setRecvByteBufAllocator(new FixedRecvByteBufAllocator(ConfigManager.getInstance().getRecvByteBufAllocator()));
}
// -----------------------------------------------------------
// Add string encoder (downstream) / string decoder (upstream)
// -----------------------------------------------------------
// For decoding from a ChannelBuffer to a String object
String stringDecoderName = "SERVER_PIPELINE_STRING_DECODER_" + theSourceRouteId;
channelPipeline.addLast(stringDecoderName, STR_DECODER);
// For encoding from a String object into a ChannelBuffer
String stringEncoderName = "SERVER_PIPELINE_STRING_ENCODER_" + theSourceRouteId;
channelPipeline.addLast(stringEncoderName, STR_ENCODER);
// For encoding from a String object into a DatagramPacket
String datagramPacketEncoderName = "SERVER_PIPELINE_DATAGRAM_PACKET_ENCODER_" + theSourceRouteId;
channelPipeline.addLast(datagramPacketEncoderName, DATAGRAM_PACKET_ENCODER);
// -----------------------------------------------------------------------------------------
// Default Camel ServerChannelHandler for the consumer, to allow Camel to route the message.
// -----------------------------------------------------------------------------------------
String serverChannelHandlerName = "SERVER_CHANNEL_HANDLER_" + theSourceRouteId;
channelPipeline.addLast(serverChannelHandlerName, new ServerChannelHandler(consumer));
}
I've got a controller defined:
class FamilyController extends RestfulController {
def springSecurityService
def familyService
static responseFormats = ['json']
FamilyController() {
super(Family)
}
#Override
protected List<Family> listAllResources(Map params) {
def user = springSecurityService.loadCurrentUser()
return Family.findAllByUser(user)
}
#Override
protected Family createResource() {
//def instance = super.createResource()
//TODO: Should be able to run the above line but there is an issue GRAILS-10411 that prevents it.
// Code from parent is below, as soon as the jira is fixed, remove the following lines:
def p1 = params
def p2 = getObjectToBind()
Family instance = resource.newInstance()
bindData instance, getObjectToBind()
/*
* In unit testing, the bindData operation correctly binds the params
* data to instance properties. When running with integration tests,
* this doesn't happen and I have to do the property assignment myself.
* Not sure if this breaks anything elsewhere.
*/
instance.properties = p1
//Code from super ends here
familyService.addFamilyToUser(instance)
return instance
}
#Override
protected Family queryForResource(Serializable id) {
def inst = familyService.safeGetFamily(Long.parseLong(id))
return inst
}
public create()
{
Family r = this.createResource()
render r as JSON
}
public index()
{
render this.listAllResources(params) as JSON
}
}
And for pedantic purposes (I'm new to grails/groovy and I want to watch the controller work), I want to take a look at actual results from the various actions in the controller. So I built an integration test:
#Mock([AdultPlanning, Primary, Secondary, Child, Family, Tenant, User])
class FamilyControllerIntegrationSpec extends IntegrationSpec {
Tenant tenant
User user
FamilyController controller = new FamilyController()
FamilyService familyService = new FamilyService()
def setupSpec() {
tenant = new Tenant(name: 'MMSS')
user = new User(subject: "What is a subject?", identityProvider: "http://google.com/")
tenant.addToUsers(user)
tenant.save()
user.save() // The tenant save should propagate to the user, this is to make sure.
/*
* Set up the custom marshalling code for family objects.
*/
new AdultPlanningMarshaller()
new ChildMarshaller()
new FamilyMarshaller()
new FamilyTypeEnumMarshaller()
return
}
def cleanup() {
}
void "Create a Family"() {
when:
def mSpringSecurityService = mockFor(SpringSecurityService)
/*
* I expect to make n calls (1..3) on loadCurrentUser in this test.
*/
mSpringSecurityService.demand.loadCurrentUser(1..3) { -> return user }
controller.springSecurityService = mSpringSecurityService.createMock()
controller.request.contentType = 'text/json'
controller.index()
def json
try {
json = controller.response.json
}
catch (e)
{
json = null // action didn't emit any json
}
def text = controller.response.text
then:
!json
text == "[]"
when:
controller.params.name = "Test"
controller.params.typeOfFamily = FamilyTypeEnum.SINGLE
controller.familyService.springSecurityService = controller.springSecurityService //mSpringSecurityService.createMock()
/*
* I've tried both GET (default) and POST to get bindData to pick up parameters properly,
* with no joy.
*/
// controller.request.method = "POST"
controller.request.contentType = 'text/json'
controller.create()
try {
json = controller.response.json
}
catch (e)
{
json = null // action didn't emit any json
}
text = controller.response.text
then:
1 == 1
when:
controller.index()
json = controller.response.json
then:
json.name == "Test"
}
}
So in the first when block, I get no families listed (and empty json object and a string of [] in the response), which I expected, but until I actually implemented the index action in code, I got no json back from the action which I didn't expect.
On to the 2nd when. I create a family (problems there with dataBinding but I tossed in a workaround, first things first) and the family gets created and saved, but nothing comes back in the response. I've used the debugger and I see the JSON marshaller triggering for the newly created family so SOMETHING is rendering JSON but where is it going?.
So I'm clearly doing something wrong (or I don't understand what actions are supposed to do in terms of what they return for passing on to the web client) but I'm new in this domain and don't have a clue as to what was done wrong.
I am the admin of an enterprise account at Box, and I'm working on an automated integration to update our users' email addresses and set their quotas, based on our enterprise' internal catalog.
Although the Box API documentation seems targeted at other usage scenarios, I can gather that once I get an access_token/refresh_token pair, that refresh_token is valid for 60 days, and I can get a new one at any time during that period.
Being of the conviction that "something always goes wrong", I'm just wondering if there is any way of automating the initial step of getting an access_token/refresh_token pair, that doesn't require a browser and manual interaction. I'm afraid that IF the refresh_token is lost or becomes invalid due to an update at Box or similar, no one here will remember how you went about getting that initial token pair by hand.
If there isn't a way to do it automatically, I'll just live with it, but I don't want to give up without having asked explicitly to know that I didn't just miss something. :-)
[Is there] any way of automating the initial step of getting an access_token/refresh_token pair, that doesn't require a browser and manual interaction
No, there are no authZ/authN shortcuts. That goes double for accounts that can manage an entire enterprise, given their power and reach.
I'm afraid ... no one here will remember how you went about getting that initial token pair by hand.
One way to resolve this might be to implement something like this:
Create a Box app with the 'manage an enterprise' scope.
Create a web app in your domain that simply implements the OAuth2 workflow.
Store the resulting access/refresh token pair in your persistence layer of choice
If/when something goes wrong due to authZ/authN issues, have your script notify a group email account that someone needs to go to the web app and request a new token.
There are sample web apps available to help get you started. (Python, Asp.NET MVC)
... The Box API documentation seems targeted at other usage scenarios...
A lot of the enterprise-specific stuff is found in the Users and Events parts of the API, and the As-User feature makes the entire API enterprise-ready. It's pretty neat.
You can build a workarround with an webclient like this:
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.ExecutionException;
import com.gargoylesoftware.htmlunit.BrowserVersion;
import com.gargoylesoftware.htmlunit.ElementNotFoundException;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.html.HtmlButton;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.html.HtmlPasswordInput;
import com.gargoylesoftware.htmlunit.html.HtmlSubmitInput;
import com.gargoylesoftware.htmlunit.html.HtmlTextInput;
public class BoxAuth {
private String key;
private String email;
private String password;
private String redirectUrl;
private final String AUTH_URL;
public BoxAuth(String key, String email, String password, String redirectUrl) {
super();
this.key = key;
this.email = email;
this.password = password;
this.redirectUrl = redirectUrl;
this.AUTH_URL = "https://www.box.com/api/oauth2/authorize?response_type=code&client_id=" + key + "&redirect_uri=" + this.redirectUrl;
}
public String authorize() throws IOException, InterruptedException, ExecutionException {
System.out.println("AUTHORIZING: " + AUTH_URL);
final WebClient webClient = new WebClient(BrowserVersion.FIREFOX_17);
HtmlPage loginPage = webClient.getPage(AUTH_URL);
final HtmlPage grantAccessePage = this.authorizeLogin(loginPage);
return this.authorizeGrantAccess(grantAccessePage);
}
private HtmlPage authorizeLogin(HtmlPage page) throws IOException {
final HtmlForm loginForm = page.getFormByName("login_form");
loginForm.getInputByName("password");
final HtmlTextInput emailField = (HtmlTextInput) loginForm.getInputByName("login");
emailField.setValueAttribute(this.email);
final HtmlPasswordInput passwordField = (HtmlPasswordInput) loginForm.getInputByName("password");
passwordField.setValueAttribute(this.password);
final HtmlSubmitInput loginButton = loginForm.getInputByName("login_submit");
final HtmlPage result = loginButton.click();
try {
final HtmlForm test = result.getFormByName("login_form");
throw new Exception("BoxAPI: Wrong login data!!!");
} catch (ElementNotFoundException e) {
}
return result;
}
private String authorizeGrantAccess(HtmlPage grantAccessePage) throws IOException, InterruptedException, ExecutionException {
final HtmlForm grantAccessForm = grantAccessePage.getHtmlElementById("consent_form");
final HtmlButton grantAccess = grantAccessForm.getButtonByName("consent_accept");
final HtmlPage codePage = grantAccess.click();
URL url = codePage.getUrl();
String result = "";
if (url.toString().contains("&code=")) {
result = url.toString().substring(url.toString().indexOf("&code="));
result = result.replace("&code=", "");
}
return result;
}
}
as redirect_url u can use something like "https://app.box.com/services/yourservice"