I have a webview activity that loads a URL with a few custom request headers in its onCreate() method. The requirement is to pass the custom headers with the initial URL request. On a few devices, the webview stops sending the headers after the webview activity has been launched a few times.
For example, I have a HomeActivity which launches a WebViewActivity. After launching the WebViewActivity and navigating back to HomeActivity a few times, the WebViewActivity stops sending the custom request headers and this behaviour doesn't change unless I clear the application's data.
I have confirmed this behaviour using a MITM tool. The implementation is as follows:
#Override
protected void onCreate(Bundle savedInstanceState) {
Map<String, String> map = new HashMap<>();
map.put("header1", "header1_value");
map.put("header2", "header2_value");
map.put("header3", "header3_value");
map.put("header4", "header4_value");
webView.loadUrl("https://www.example.com/mypath", map);
}
The above snippet executes unconditionally on every activity launch. However, the headers are not present in the actual request made by the webview. Also, the page being requested is a 303 redirect.
If your minimum API target is level 21, you can use the shouldInterceptRequest else you can use this
With each interception, you will need to take the url, make this request yourself, and return the content stream:
Then:
WebViewClient wvc = new WebViewClient() {
#Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
try {
DefaultHttpClient client = new DefaultHttpClient();
HttpGet httpGet = new HttpGet(url);
httpGet.setHeader("header1", "header1_value");
httpGet.setHeader("header2", "header2_value");
httpGet.setHeader("header3", "header3_value");
httpGet.setHeader("header4", "header4_value");
HttpResponse httpReponse = client.execute(httpGet);
Header contentType = httpReponse.getEntity().getContentType();
Header encoding = httpReponse.getEntity().getContentEncoding();
InputStream responseInputStream = httpReponse.getEntity().getContent();
String contentTypeValue = null;
String encodingValue = null;
if (contentType != null) {
contentTypeValue = contentType.getValue();
}
if (encoding != null) {
encodingValue = encoding.getValue();
}
return new WebResourceResponse(contentTypeValue, encodingValue, responseInputStream);
} catch (ClientProtocolException e) {
//return null to tell WebView we failed to fetch it WebView should try again.
return null;
} catch (IOException e) {
//return null to tell WebView we failed to fetch it WebView should try again.
return null;
}
}
}
//Where wv is your webview
wv.setWebViewClient(wvc);
Based on this question
Related
I am attempting to implement a technique similar to the one describe in this question.
I have an android application (Ionic built on top of Cordova) that runs in a webview. Basically what I want to do is load a page into an iframe and perform some work on this page. Many website uses the X-Frame-Options: DENY header to disallow their content from being loaded in an iFrame. In a chrome extension you can get around this by intercepting the webrequest and removing that header.
I've overridden the shouldInterceptRequest function here: https://developer.android.com/reference/android/webkit/WebViewClient.html
// Handle API until level 21
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
#Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
try {
WebResourceResponse cordovaResponse = super.shouldInterceptRequest(view, request);
if(cordovaResponse != null) {
return cordovaResponse;
}
String url = request.getUrl().toString();
HttpURLConnection urlConnection = (HttpURLConnection) new URL(url).openConnection();
urlConnection.connect();
//view.loadUrl(url, getCustomHeaders());
WebResourceResponse response = new WebResourceResponse(urlConnection.getContentType(),
urlConnection.getContentEncoding(),
urlConnection.getInputStream());
Map<String, String> headers = response.getResponseHeaders();
if(headers != null){
response.setResponseHeaders(removeXOriginHeaders(headers));
}
return response;
} catch(MalformedURLException e) {
e.printStackTrace();
return null;
}
catch (IOException e) {
e.printStackTrace();
return null;
}
}`
but when the headers for all requests are received using the above method they are null and when the content is put into the iframe, it doesn't result in a fully formed Document.
The chrome debugger provides this message: Resource interpreted as Document but transferred with MIME type text/html;charset=UTF-8:
It's like the page content is fetched using xhr and then stuck inside a single element of the Document as opposed to loading as it normally would when using an iframe (all scripts run to execution, subsequent ajax requests fired etc).
Is there anyway to get the page content to load in the iframe after having removed that single header?
I was able to solve my problem by using the OkHttpClient found here: http://square.github.io/okhttp/ instead of the java URLConnection
// Handle API until level 21
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
#Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
try {
WebResourceResponse cordovaResponse = super.shouldInterceptRequest(view, request);
if(cordovaResponse != null) {
return cordovaResponse;
}
String url = request.getUrl().toString();
OkHttpClient httpClient = new OkHttpClient();
Request okRequest = new Request.Builder()
.url(url)
.build();
Response response = httpClient.newCall(okRequest).execute();
Response modifiedResponse = response.newBuilder()
.removeHeader("x-frame-options")
.removeHeader("frame-options")
.build();
return new WebResourceResponse("text/html",
modifiedResponse.header("content-encoding", "utf-8"),
modifiedResponse.body().byteStream()
);
} catch(MalformedURLException e) {
e.printStackTrace();
return null;
}
catch (IOException e) {
e.printStackTrace();
return null;
}
}
So, I have this webpage which I want to access, but first I have to login from another webpage. I want to keep the cookies and then use it for later automatic login. So far what I did:
First, this is the login webpage: https://autenticacao.uvanet.br/autenticacao/pages/login.jsf
It's my university's student's area.
public class Consulta extends AsyncTask<String, Void, String> {
#Override
protected String doInBackground(String... urls) {
StringBuilder builder = new StringBuilder(100000);
DefaultHttpClient client = new DefaultHttpClient();
HttpPost httpPost = new HttpPost(urls[0]);
try {
List<NameValuePair> val = new ArrayList<NameValuePair>(2);
val.add(new BasicNameValuePair("form:usuario", "myusername"));
val.add(new BasicNameValuePair("form:senha", "mypass"));
httpPost.setEntity(new UrlEncodedFormEntity(val));
HttpResponse response = client.execute(httpPost);
InputStream content = response.getEntity().getContent();
BufferedReader buffer = new BufferedReader(new InputStreamReader(content));
String s = "";
while ((s = buffer.readLine()) != null) {
builder.append(s);
}
} catch (Exception e) {
e.printStackTrace();
}
return builder.toString();
}
}
This is the class I use to make the HttpPost and this is how I call it:
#Override
public void onClick(View v) {
try{
String html = new Consulta().execute("https://autenticacao.uvanet.br/autenticacao/pages/login.jsf").get();
Document doc = Jsoup.parse(html);
Element link = doc.select("title").first();
String t = link.text();
tv1.setText(t);
}catch(Exception e){
e.printStackTrace();
}
}
I believed it would work this way:
I send the webpage to login to Consulta.java
The class would get the fields "form:usuario" and "form:senha" and fill them with myusername and mypassword and then login
The class would return me html code of the second webpage as string
But what happens is that it returns me the first webpage (the login one). I'm pretty sure I'm doing something wrong, but I don't know what, could someone help me? Also, sorry for my english, it's not my main language.
When you do the login (in https://autenticacao.uvanet.br/autenticacao/pages/login.jsf), I don't think the response is the html code of the second webpage. Are you sure about this?
I think the normal behavior for a login page is to respond with the same page (the login one) but adding the session cookie and the header to do a redirect to the second webpage, but not the second page itself.
In this case, you have to read the http header response to extract these parameters: the cookies and the URL of the second webpage.
Using the object HttpResponse:
Header[] h = response.getAllHeaders();
But I recommend you to use HttpURLConnection class instead of DefaultHttpClient.
I'm using HttpURLConnection to do communication with a backend server and im doing so in an async task in the doInBackground method as you should.
Now I need to be able to follow 302 redirects, but I'm having some problems with this. The issue is that the new location usually will be on another host, however when doing the redirect request it seem not to change the URL to a new host hence I get a 404 error saying the specified path does not exits.
Now I know I could set HtppURLConnection.setFollowRedirect but I need to have more control over the redirects so they should not just be followed blindly. The Redirect behavour should be controlled by the object who called the asynctask (when an asynctask object is created you pass the object who creates it in a parameter called _callback).
Heres's my current code:
protected HttpResponse doInBackground(String... req) {
HttpURLConnection urlConnection = null;
try {
urlConnection = (HttpURLConnection) this._url.openConnection();
urlConnection.setConnectTimeout( (int) this._timeout*1000);
String body = req[0];
// set headers / write information to output stream if request is post
// create the response object
HttpResponse responseObject = null;
try
{
// get status, contenttype, charset...
InputStream in = null;
if (urlConnection.getResponseCode() != -1 && urlConnection.getResponseCode() < 300)
{
in = new BufferedInputStream(urlConnection.getInputStream(), 8192);
}
else
{
in = new BufferedInputStream(urlConnection.getErrorStream(), 8192);
}
responseObject = new HttpResponse(in, status, contentType, charset);
// if redirect
if (status == 302 && this._callback.onRedirect(responseObject) == true)
{
// recall
String url = urlConnection.getHeaderField("location");
Log.v("Async Task", "Redirect location: " + url);
this._url = null;
this._url = new URL(url);
urlConnection.disconnect();
urlConnection = null;
responseObject = this.doInBackground(req);
}
} catch (IOException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
// return the response
return responseObject;
}
// catch some other exceptions
finally
{
if (urlConnection != null)
{
urlConnection.disconnect();
} }
}
And as said the problem is that the redirect request seem to change the path of the URL but not the host. The URL object itself seem to contain the right information so I have no idea why this is happening. (I'm getting HTML as response which is an 404 error page that includes the server name of the old server)
Thanks for any help!
Note: HttpResponse is just an object I created for holding the relevant information about the response.
This was caused by the fact that I sent the same headers and did not change the "host" header of the request which caused Apache to be confused it seems.
Is there a way to view the http response headers in an Activity once a web page has been loaded in a WebView? Seems like this should be possible, but I can't find any methods that expose the headers.
Neither WebView nor WebViewClient provide methods to do that, Though, you can try to implement that manually. You can do something like this:
private WebView webview;
public void onCreate(Bundle icicle){
// bla bla bla
// here you initialize your webview
webview = new WebView(this);
webview.setWebViewClient(new YourWebClient());
}
// this will be the webclient that will manage the webview
private class YourWebClient extends WebViewClient{
// you want to catch when an URL is going to be loaded
public boolean shouldOverrideUrlLoading (WebView view, String urlConection){
// here you will use the url to access the headers.
// in this case, the Content-Length one
URL url;
URLConnection conexion;
try {
url = new URL(urlConection);
conexion = url.openConnection();
conexion.setConnectTimeout(3000);
conexion.connect();
// get the size of the file which is in the header of the request
int size = conexion.getContentLength();
}
// and here, if you want, you can load the page normally
String htmlContent = "";
HttpGet httpGet = new HttpGet(urlConection);
// this receives the response
HttpResponse response;
try {
response = httpClient.execute(httpGet);
if (response.getStatusLine().getStatusCode() == 200) {
// la conexion fue establecida, obtener el contenido
HttpEntity entity = response.getEntity();
if (entity != null) {
InputStream inputStream = entity.getContent();
htmlContent = convertToString(inputStream);
}
}
} catch (Exception e) {}
webview.loadData(htmlContent, "text/html", "utf-8");
return true;
}
public String convertToString(InputStream inputStream){
StringBuffer string = new StringBuffer();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line;
try {
while ((line = reader.readLine()) != null) {
string.append(linea + "\n");
}
} catch (IOException e) {}
return string.toString();
}
}
I can't test it right now, but that's basically what you could do (it's very crazy though :).
inspired by Cristian answer I needed to intercept AJAX calls webview is doing, where I needed to intercept response headers to get some information (cart item count in e-commerce app), which I needed to leverage in app. As the app is using okhttp I've ended up doing this and it's working:
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
#Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
Log.i(TAG,"shouldInterceptRequest path:"+request.getUrl().getPath());
WebResourceResponse returnResponse = null;
if (request.getUrl().getPath().startsWith("/cart")) { // only interested in /cart requests
returnResponse = super.shouldInterceptRequest(view, request);
Log.i(TAG,"cart AJAX call - doing okRequest");
Request okRequest = new Request.Builder()
.url(request.getUrl().toString())
.post(null)
.build();
try {
Response okResponse = app.getOkHttpClient().newCall(okRequest).execute();
if (okResponse!=null) {
int statusCode = okResponse.code();
String encoding = "UTF-8";
String mimeType = "application/json";
String reasonPhrase = "OK";
Map<String,String> responseHeaders = new HashMap<String,String>();
if (okResponse.headers()!=null) {
if (okResponse.headers().size()>0) {
for (int i = 0; i < okResponse.headers().size(); i++) {
String key = okResponse.headers().name(i);
String value = okResponse.headers().value(i);
responseHeaders.put(key, value);
if (key.toLowerCase().contains("x-cart-itemcount")) {
Log.i(TAG,"setting cart item count");
app.setCartItemsCount(Integer.parseInt(value));
}
}
}
}
InputStream data = new ByteArrayInputStream(okResponse.body().string().getBytes(StandardCharsets.UTF_8));
Log.i(TAG, "okResponse code:" + okResponse.code());
returnResponse = new WebResourceResponse(mimeType,encoding,statusCode,reasonPhrase,responseHeaders,data);
} else {
Log.w(TAG,"okResponse fail");
}
} catch (IOException e) {
e.printStackTrace();
}
}
return returnResponse;
}
I hope this may be helpful to others and if somebody has a suggestions for improvement I would be grateful. Unfortunately it's compatible only with LOLLIPOP and higher as from this version you can access/return headers using WebResourceRequest, which was needed for my case.
You should be able to control all your headers by skipping loadUrl and writing your own loadPage using Java's HttpURLConnection. Then view the headers, do your thing, and use the webview's loadData to display the response.
There is an alternative solution if you're targeting at least Kit-Kat, even though this wouldn't show the headers in the Activity but rather in Chrome. You can simply follow this short guide on how to remotely debug Webviews.
The 2 key points are, first, to enable WebView debugging in you app
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
WebView.setWebContentsDebuggingEnabled(true);
}
And then open chrome://inspect in a Chrome tab in a computer. Once you connect your phone via USB to the computer, you will see your app's WebView in the list of debuggable devices
you can use OkHttp:
private fun handleRequestViaOkHttp(url: String) {
var httpClient = OkHttpClient()
thread {
try {
val request = Request.Builder().url(url).build()
print("Request: $request")
val response = httpClient.newCall(request).execute()
println("Response: " + response.headers().toString())
} catch (e: Exception) {}
}
}
you should call it inside this method:
override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
handleRequestViaOkHttp(webViewUrl.value.toString())
return super.shouldInterceptRequest(view, request)
}
As the accepted answer will only work with HttpGet, here is a trick thet currently I'm using (at this time it seems to work)
In onPageFinished handler, if there is an error, the title of the page will be like "ERROR_NUM - ERROR_DESCRIPTION", like "500 - Internal Server Error", so all I do is to get title from webview in the function, and then check the title.
view.getTitle()
Okay, I have an android application that has a form in it, two EditText, a spinner, and a login button. The user selects the service from the spinner, types in their user name and password, and clicks login. The data is sent via POST, a response is returned, it's handled, a new WebView is launched, the html string generated from the response is loaded, and I have the home page of whatever service the user selected.
That's all well and good. Now, when the user clicks on a link, the login info can't be found, and the page asks the user to login again. My login session is being dropped somewhere, and I'm not certain how to pass the info from the class that controls the main part of my app to the class that just launches the webview activity.
The onClick handler from the form login button:
private class FormOnClickListener implements View.OnClickListener {
public void onClick(View v) {
String actionURL, user, pwd, user_field, pwd_field;
actionURL = "thePageURL";
user_field = "username"; //this changes based on selections in a spinner
pwd_field = "password"; //this changes based on selections in a spinner
user = "theUserLogin";
pwd = "theUserPassword";
List<NameValuePair> myList = new ArrayList<NameValuePair>();
myList.add(new BasicNameValuePair(user_field, user));
myList.add(new BasicNameValuePair(pwd_field, pwd));
HttpParams params = new BasicHttpParams();
DefaultHttpClient client = new DefaultHttpClient(params);
HttpPost post = new HttpPost(actionURL);
HttpResponse response = null;
BasicResponseHandler myHandler = new BasicResponseHandler();
String endResult = null;
try { post.setEntity(new UrlEncodedFormEntity(myList)); }
catch (UnsupportedEncodingException e) { e.printStackTrace(); }
try { response = client.execute(post); }
catch (ClientProtocolException e) { e.printStackTrace(); }
catch (IOException e) { e.printStackTrace(); }
try { endResult = myHandler.handleResponse(response); }
catch (HttpResponseException e) { e.printStackTrace(); }
catch (IOException e) { e.printStackTrace(); }
List<Cookie> cookies = client.getCookieStore().getCookies();
if (!cookies.isEmpty()) {
for (int i = 0; i < cookies.size(); i++) {
cookie = cookies.get(i);
}
}
Intent myWebViewIntent = new Intent(MsidePortal.this, MyWebView.class);
myWebViewIntent.putExtra("htmlString", endResult);
myWebViewIntent.putExtra("actionURL", actionURL);
startActivity(myWebViewIntent);
}
}
And here is the WebView class that handles the response display:
public class MyWebView extends android.app.Activity {
private class MyWebViewClient extends WebViewClient {
#Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.web);
MyWebViewClient myClient = new MyWebViewClient();
WebView webview = (WebView)findViewById(R.id.mainwebview);
webview.getSettings().setBuiltInZoomControls(true);
webview.getSettings().setJavaScriptEnabled(true);
webview.setWebViewClient(myClient);
Bundle extras = getIntent().getExtras();
if(extras != null)
{
// Get endResult
String htmlString = extras.getString("htmlString");
String actionURL = extras.getString("actionURL");
Cookie sessionCookie = MsidePortal.cookie;
CookieSyncManager.createInstance(this);
CookieManager cookieManager = CookieManager.getInstance();
if (sessionCookie != null) {
cookieManager.removeSessionCookie();
String cookieString = sessionCookie.getName()
+ "=" + sessionCookie.getValue()
+ "; domain=" + sessionCookie.getDomain();
cookieManager.setCookie(actionURL, cookieString);
CookieSyncManager.getInstance().sync();
}
webview.loadDataWithBaseURL(actionURL, htmlString, "text/html", "utf-8", actionURL);}
}
}
}
I've had mixed success implementing that cookie solution. It seems to work for one service I log into that I know keeps the cookies on the server (old, archaic, but it works and they don't want to change it.) The service I'm attempting now requires the user to keep cookies on their local machine, and it does not work with this setup.
Any suggestions?
You need to keep the cookie from one call to another. Instead of creating a new DefaultHttpClient, use this builder:
private Object mLock = new Object();
private CookieStore mCookie = null;
/**
* Builds a new HttpClient with the same CookieStore than the previous one.
* This allows to follow the http session, without keeping in memory the
* full DefaultHttpClient.
* #author Régis Décamps <decamps#users.sf.net>
*/
private HttpClient getHttpClient() {
final DefaultHttpClient httpClient = new DefaultHttpClient();
synchronized (mLock) {
if (mCookie == null) {
mCookie = httpClient.getCookieStore();
} else {
httpClient.setCookieStore(mCookie);
}
}
return httpClient;
}
And keep the Builder class as a field of your application.
Use this in url login Activity
List<Cookie> cookies = client.getCookieStore().getCookies();
if (!cookies.isEmpty()) {
for (int i = 0; i < cookies.size(); i++) {
cookie = cookies.get(i);
}
}
Cookie sessionCookie = cookie;
if (sessionCookie != null) {
String cookieString = sessionCookie.getName() + "="
+ sessionCookie.getValue() + "; domain="
+ sessionCookie.getDomain();
cookieManager
.setCookie("www.mydomain.com", cookieString);
CookieSyncManager.getInstance().sync();
}
You could store the cookies in a shared preference and load them as needed in other activitys.
Or try this idea from a similar question.
dunno if you still need an answer, but again here comes some additional info that may help
if you want to keep cookies sync'ed
// ensure any cookies set by the dialog are saved
CookieSyncManager.getInstance().sync();
and if you want to clear Cookies
public static void clearCookies(Context context) {
// Edge case: an illegal state exception is thrown if an instance of
// CookieSyncManager has not be created. CookieSyncManager is normally
// created by a WebKit view, but this might happen if you start the
// app, restore saved state, and click logout before running a UI
// dialog in a WebView -- in which case the app crashes
#SuppressWarnings("unused")
CookieSyncManager cookieSyncMngr =
CookieSyncManager.createInstance(context);
CookieManager cookieManager = CookieManager.getInstance();
cookieManager.removeAllCookie();
}
When any new activity is launched (so I assume this is the same when you launch a new webview) it is effectively launching a new program from scratch. This new activity will not have access to data from any previous activity (unless that data is passed by being attached to the intent).
2 possible solutions:
1) putExtra can only be used to pass primitive data, so to pass something more complex you need to either
a) Wrap the more complex structure in a class that implements the
Parcelable interface, which can be stored in an extra.
b) Wrap the more complex structure in a class that implements the
Serializable interface, which can be stored in an extra.
Either of these approaches is fairly complicated and a fair bit of work.
2)Personally I much prefer the approach suggested by rds. To clarify, when rds says:
keep the Builder class as a field of your application.
I think he means extend the application class. Any properties stored there are available globally to all activities.
This article explains very clearly how to do this:
http://www.screaming-penguin.com/node/7746
You can ignore the stuff about the AsyncTask (although I'm sure you will find a need for that at some point) and just concentrate on the part about extending the application class.
I have this similiar problem several week ago, that is because you create new DefaultHttpClient each time you click the button.. try create one DefaultHttpClient, and using the same DefaultHttpClient for each request you trying to send. it solved my problem
You have used this line -
if (sessionCookie != null) {
cookieManager.removeSessionCookie();
}
To ensure you receive new cookie everytime.
Seems like you have gone through same issue as I faced, check below link -
removeSessionCookie() issue of android (code.google.,com)
it says that removeSessionCookie() is implemented in a thread, so whenever it is called; a thread starts and after your setCookie(url, cookieString); is called, it removes the new cookie you just set.
So for some devices it works well as removeSessionCookie() is already executed, while, for some, it remove the cookie, and we get that problem.
I suggest you remove this removeSessionCookie(); as you are setting only one cookie, so it won't conflict with other cookies. Your code will work seamlessly.
Sever use the sessionID stored in the cookies to identify the specfic user.Every language has a different sessionID name in the http headers.You can use some network tool or browser to see what is the name the sessionID called.
And other way,I GUESS,the facebook and twitter way,you will remove all the session-related code, it's server use Access Token to identify a user.
Am i clear?