I'm building a login page that, upon submitting and validation of the user credentials, opens up a native mobile application. Up till last week, I had this working cross mobile OS by using a custom scheme URI, something like:
function onLoginCallback() {
const redirectUri = 'customscheme:/action?param1=val1¶m2=val2';
window.location.replace(redirectUri);
}
The login page is displayed in an IABT, short for In App Browser Tab.
However, since the release of version 61 of Chrome, this is approach is broken on Android. Chrome blocks the redirect because there's no apparent user action related to the redirect (see here for more information on the matter).
As a consequence, when executing the code above, I'll end up with a warning in the console:
Navigation is blocked: customscheme:/action?param1=val1¶m2=val2
I've also tried updating the custom scheme url to an intent url but to no avail. Googling about this issue doesn't readily provide a clear solution, so I'm hoping anyone on can help me out.
Edit: Tried to reproduce the issue with the following scenario (as close as possible to the real life scenario):
IABT displays a page with a single button
Clicking the button fires an jsonp call to a mock endpoint
The JSONP callback is executed and fires off a custom event
An event handler for the custom event is triggered and redirects the browser to another mock endpoint
That mock endpoint responds with a 302 to the custom deeplink scheme.
Alas, this seems to be working. I would have expected that the inclusion of the jsonp call would cause Chrome to block the final redirect as it would not be able to identify it as a user initiated action.
Edit 2: Managed to get a reproducible scenario. We've set up a dummy endpoint, that upon request simply returns a 302 with the custom scheme in the Location header. This is blocked on all tries, except for the first one. That fact still boggles the mind. We're using the AppAuth for Android application to test the setup.
I'm opening a custom tab to the endpoint as shown below. The code is taken from this answer.
void launchTab(Context context, Uri uri){
final CustomTabsServiceConnection connection = new CustomTabsServiceConnection() {
#Override
public void onCustomTabsServiceConnected(ComponentName componentName, CustomTabsClient client) {
final CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
final CustomTabsIntent intent = builder.build();
client.warmup(0L); // This prevents backgrounding after redirection
intent.launchUrl(context, uri);
}
#Override
public void onServiceDisconnected(ComponentName name) {
}
};
CustomTabsClient.bindCustomTabsService(context, "com.android.chrome", connection);
}
We ended up implementing our login and registration forms with a classic post-redirect-get pattern.
The server responds with a 302 to the custom URI scheme. Because in this setup there's no asynchronous execution between the user submitting the form and the browser receiving a redirect, Chrome correctly identifies the chain of actions as trusted and thus will not block the navigation.
I realise this might not be the preferred solution for everyone. A possible alternative to support asynchronous execution flows is the use of universal links as these use regular http(s) schemes, to which redirects were (at the time of posting my question) not considered harmful by Chrome.
For those who use App Auth client and Identity Server:
Startup.cs
services.AddTransient<IAuthorizeResponseGenerator, AuthorizeRG>();
AuthorizeRG.cs
public class AuthorizeRG: AuthorizeResponseGenerator
{
public override async Task<AuthorizeResponse> CreateResponseAsync(ValidatedAuthorizeRequest request)
{
var response = await base.CreateResponseAsync(request);
if (response.RedirectUri != null && request.IsNativeClient())
//this fix chrome navigation blocked on native clients https://bugs.chromium.org/p/chromium/issues/detail?id=738724
response.Request.RedirectUri = $"/native/redirect/{HttpUtility.UrlEncode(response.RedirectUri)}";
return response;
}
}
NativeController.cs
[Route("[controller]")]
public class NativeController : Controller
{
[HttpGet("Redirect/{redirectUri}")]
public IActionResult Redirect([FromRoute] string redirectUri)
{
redirectUri = HttpUtility.UrlDecode(redirectUri);
redirectUri += HttpContext.Request.QueryString.ToUriComponent();
return this.LoadingPage("Redirect", redirectUri);
}
}
Extensions.cs
/// <summary>
/// Checks if the redirect URI is for a native client.
/// </summary>
/// <returns></returns>
public static bool IsNativeClient(this AuthorizationRequest context)
{
return !context.RedirectUri.StartsWith("https", StringComparison.Ordinal)
&& !context.RedirectUri.StartsWith("http", StringComparison.Ordinal);
}
public static bool IsNativeClient(this ValidatedAuthorizeRequest context)
{
return !context.RedirectUri.StartsWith("https", StringComparison.Ordinal)
&& !context.RedirectUri.StartsWith("http", StringComparison.Ordinal);
}
public static IActionResult LoadingPage(this Controller controller, string viewName, string redirectUri)
{
controller.HttpContext.Response.StatusCode = 200;
controller.HttpContext.Response.Headers["Location"] = "";
return controller.View(viewName, new RedirectViewModel { RedirectUrl = redirectUri });
}
This works for me, but please comment if it broke smth in your authorization flow
Related
I have a Java application that as part of it's output creates an html report ,then opens the html reports in users browser. On Firefox and Google Chrome this works but on Safari it opens the report as if Javascript was not enabled, even though it is. However if you reopen the report by clicking on a link from another webpage (which lists all reports) then it opens fine in Safari.
What do I need to do to trigger Safari to open the report with Javascript enabled.
Console shows some errors, but I dont understand them
This is related issue https://apple.stackexchange.com/questions/361245/safari-kcferrordomaincfnetwork-error-1-on-local-html-files but doesn't provide a satisafactory answer.
Actually the answer here https://apple.stackexchange.com/questions/366448/safari-giving-kcferrordomaincfnetwork-error-303-when-visiting-a-site about removing my site from Preferecnes:Privacy works but that is no good because the problem occurs on new computer after running the program only a few times so would have to continually do it.
You open the file locally. Browsers usually restrict local file processing for security reasons. You must "Disable local file restrictions" in Safari in the "Developer" menu for making this possible. I am not sure, but it might be necessary to do this each time Safari opens such a file via your application. Embedding all external resources might also help, but I am not sure.
Opening the remote URL should always work. So this would be the best option. As an alternative you could serve the file to the browser via an embedded HTTP server in your application.
Edit: i just saw the previous answer proposed the same workaround, but i think copy paste code is always delicious. As for your problem, i verified this is occuring on safari and nothing else, and for me this fix worked.
Edit 2: According to this answer, it is a bug in Safari that it works when you open it via hyperlink. Without changing the developer settings in every users browser, it won't be working except on a local server. I'm sorry but it seems there is no other in-code workaround except what i already provided.
If you want to open with scripts at any cost, you can to the following workaround:
Write a minimalistic server socket (or copy paste my code):
Index.java:
package index;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
public class Index extends Base {
public byte[] buildResponseBody(String resource) {
try {
System.out.println(resource);
if(resource.contains("?")) return "<!DOCTYPE HTML><html><h1>Error 404: Page not found</h1></html>".getBytes(StandardCharsets.US_ASCII);
return Files.readAllBytes(Paths.get(resource));
} catch (Exception e) {
return "<!DOCTYPE HTML><html><h1>Error 404: Page not found</h1></html>".getBytes(StandardCharsets.US_ASCII);
}
}
public static void main(String[] args) throws IOException {
new Index().loop(80);
}
}
Base.java:
package index;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public abstract class Base {
public abstract byte[] buildResponseBody(String resource);
String receiveRequest(BufferedReader reader) throws IOException {
final Pattern getLinePattern = Pattern.compile("(?i)GET\\s+/(.*?)\\s+HTTP/1\\.[01]");
String resource = null;
try {
for (String line = reader.readLine(); !line.isEmpty(); line = reader.readLine()) {
Matcher matcher = getLinePattern.matcher(line);
if (matcher.matches()) {
resource = matcher.group(1);
}
}
} catch (NullPointerException e) {
return null;
}
return resource;
}
public void sendResponse(String resource, OutputStream output, PrintWriter writer) throws IOException {
byte[] responseBody = buildResponseBody(resource);
if (responseBody == null) {
writer.println("HTTP/1.0 404 Not found");
responseBody = "Not found".getBytes();
} else
writer.println("HTTP/1.0 200 OK");
writer.println("Server: " + getClass().getSimpleName());
writer.println("Content-Length: " + responseBody.length);
writer.println();
writer.flush();
output.write(responseBody);
}
public void loop(int port) throws IOException {
try (ServerSocket serverSocket = new ServerSocket(port)) {
while (true)
try (Socket socket = serverSocket.accept();
InputStream input = socket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(input));
OutputStream output = socket.getOutputStream();
PrintWriter writer = new PrintWriter(new OutputStreamWriter(output))) {
sendResponse(receiveRequest(reader), output, writer);
}
}
}
}
This enables opening a page at 127.0.0.1:80. If you now make sure the document is accessable to your application, you can make it open a browser window at your localhost:80/file.html. Since it now runs over an actual website not just a file in safari, the scripts should theoretically be working. (At least mine are in this code)
You might want to improve security and remove some bugs of this code. I hope i could help.
I am making an ebook reader which uses epub format to load books into webviews.
In some of the books there is an anchor link to some portions in the same chapter. Each chapter is loaded as html. This is how the link look like
file:///storage/sdcard0/Android/data/com.abc.reader/files/Download/498935/epub/resources/498935/OEBPS/#footnote-165093-1-backlink
I tried using shouldOverrideUrlLoading() method to get the call back , but it's not getting called and when I press the links in onPageFinished the url shown as about:blank
reader.setWebViewClient(new WebViewClient() {
#Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
Log.w("TESTTESTOVERRIDE "+url);
view.loadUrl(url);
return false;
}
#Override
public void onPageFinished(WebView view, String url) {
// after the data has been loaded, the following is executed
// TODO Auto-generated method stub
super.onPageFinished(view, url);
System.out.println("check.... onPageFinishedEntered.."
+ url.toString());
view.loadUrl(jsfileloadurl);
}
Any ideas?
EDIT: In 4.1 devices I get the anchor links correctly,but in 4.4 or 5.0 it is about:blank. (in both cases shouldOverrideUrlLoading is not called)
I haven't tested this programmatically but I believe you are facing this issue because there was major changes in how webview works post OS 4.4 . You should check this link
https://developer.android.com/guide/webapps/migrating.html#URLs
Under section 'Custom Url Handling' it says that shouldOverrideUrlLoading() will not be invoked for invalid url. Ideally file:// should be treated as valid url but seems like it's not happening here.
One possible solution is to load you main webview content with loadDataWithBaseURL
and provide baseurl as some test url e.g. http://mytestap.testurl , it will guarantee shouldOverrideUrlLoading will get invoked all time. As a next step you need to remove prefix 'http://mytestap.testurl' if exist in the received url in shouldOverrideUrlLoading callback.
In my case it didn't work because of POST requests on web page. shouldOverrideUrlLoading:
Note: This method is not called for POST requests.
Note: This method may be called for subframes and with non-HTTP(S)
schemes; calling WebView#loadUrl(String) with such a URL will fail.
Override shouldInterceptRequest instead (one or both versions). See also https://medium.com/#madmuc/intercept-all-network-traffic-in-webkit-on-android-9c56c9262c85.
Yes. Mr. androgeek answered it rightly.
From Android OS 4.4(KK), if you implement callbacks such as shouldOverrideUrlLoading() or shouldInterceptRequest(), then WebView invokes them only for valid URLs.
If you are using Custom URL and under your control then you need to follow RFC 3986 standard to above methods called. Kindly check RFC 3986 related file:// and correct your URL
I am not sure whether the below will resolve your problem or not.
Please add below code before setting the WebViewClient
reader.getSettings().setLoadWithOverviewMode(true);
reader.getSettings().setUseWideViewPort(true);
/*This makes the layout/page rendering independent of the devices.
I use this to display local HTML pages.*/
reader.getSettings().setLayoutAlgorithm(LayoutAlgorithm.NORMAL);
In addition I have zoom controls enabled. Please note that I have tested my code from API-10 onwards with multiple devices and brands (HTC, Samsung, Nexus etc.) and found that the shouldOverrideUrlLoading works all the time.
If things do not work well, try extending the WebViewClient and Override the shouldOverrideUrlLoading method
class MyWebView extends WebViewClient{
#Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return false; //THis should be false always
}
}
Now set the WebViewClient as reader.setWebViewClient(new MyWebView());
I had implemented HttpHandler for Js extension for google api hosted jquery script files. because when it is called need to replace http with https. But visual studio started compiling javascript on pages being loaded. how do I suppress this behavior. And most interesting why did it happened.
My Http Handler :
public class HttpToHttpsHandler : IHttpHandler
{
public bool IsReusable
{
get { return false; }
}
public void ProcessRequest(HttpContext context)
{
try
{
if (context.Request.RawUrl.Contains("http:"))
{
context.Response.ContentType = "text/plain";
string newUrl = context.Request.RawUrl.Replace("http", "https");
context.Server.Transfer(newUrl);
}
}
catch (Exception)
{
throw;
}
}
}
What possibly went wrong.
It is because you have used Server.Transfer() which doesn't issue redirect to the browser but changes the execution path on the server - in the result ASP.NET will try to create something out of your JavaScript.
You want to make a simple redirection so just use Response.Redirect()
context.Response.Redirect(newUrl, false);
Also I would like suggest a more safe approach for altering the URL (in case there would be port number in URL etc.):
if (!context.Request.IsSecureConnection)
{
UriBuilder secureUriBBuilder = new UriBuilder(context.Request.Url);
secureUriBBuilder.Scheme = Uri.UriSchemeHttps;
//Ude default port for schema - alter this if your server is using custom port for HTTPS
secureUriBBuilder.Port = -1;
context.Response.Redirect(secureUriBBuilder.Uri.ToString(), false);
}
Also remember that the entire page will remain in non-safe mode if you will load the HTML in HTTP and try to load only JavaScript through HTTPS - you should consider redirection in Global.asax or usage of URL Rewrite module.
If you load up the Durandal SPA template and try to navigate to a bogus URL (by adding something like "assdf" to the URL), the url is maintained and no error is provided. How can this be changed so that invalid URLs give the user an error page? I think an error is better than leaving the user wondering if their URL worked or not.
Have you looked at the handleInvalidRoute:
http://durandaljs.com/documentation/Router/
In Hot Towel for example, it is used to show the user an error toast.
EDIT: For example, I created a basic error view, then added this to main.js and it seems to work, though I think I prefer the growl type approach to showing the error to the user in Hot Towel.
app.start().then(function() {
//Replace 'viewmodels' in the moduleId with 'views' to locate the view.
//Look for partial views in a 'views' folder in the root.
viewLocator.useConvention();
// ** ADDED **
router.handleInvalidRoute = function (route, params) {
router.navigateTo('#/error');
};
//configure routing
router.useConvention();
router.mapNav('welcome');
router.mapNav('flickr');
// ** ADDED **
router.mapRoute('error', 'viewmodels/error', 'error', false);
app.adaptToDevice();
//Show the app by setting the root view model for our application with a transition.
app.setRoot('viewmodels/shell', 'entrance');
});
EDIT2: IF you're worried about normal urls too (not hashed ones that will be handled by durandal\sammy) then you'll need to handle it outside of durandal. i.e. you can put something like the following in your global.asax:
protected void Application_Error(object sender, EventArgs e)
{
Exception exception = Server.GetLastError();
Response.Clear();
var httpException = exception as HttpException;
if(httpException != null) //It's an Http Exception, Let's handle it.
{
switch (httpException.GetHttpCode())
{
case 404:
// Page not found.
Response.Redirect("~/#/error");
break;
}
}
}
See here for a more complete approach: How can I properly handle 404 in ASP.NET MVC?
I want to create an application where a web server can get the MAC Address of the clients logging in. The only possible way I could think of was to create a JAVA applet which contains java.net methods to find the mac address
I am using javascript to call the applet methods, but the browser is not allowing those methods to execute. Below is the applet I have created.
import java.applet.*;
import java.awt.*;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
public class AppletRunner extends Applet{
// The method that will be automatically called when the applet is started
public void init()
{
// It is required but does not need anything.
}
//This method gets called when the applet is terminated
//That's when the user goes to another page or exits the browser.
public void stop()
{
// no actions needed here now.
}
//The standard method that you have to use to paint things on screen
//This overrides the empty Applet method, you can't called it "display" for example.
public void paint(Graphics g)
{
//method to draw text on screen
// String first, then x and y coordinate.
g.drawString(getMacAddr(),20,20);
g.drawString("Hello World",20,40);
}
public String getMacAddr() {
String macAddr= "";
InetAddress addr;
try {
addr = InetAddress.getLocalHost();
System.out.println(addr.getHostAddress());
NetworkInterface dir = NetworkInterface.getByInetAddress(addr);
byte[] dirMac = dir.getHardwareAddress();
int count=0;
for (int b:dirMac){
if (b<0) b=256+b;
if (b==0) {
macAddr=macAddr.concat("00");
}
if (b>0){
int a=b/16;
if (a==10) macAddr=macAddr.concat("A");
else if (a==11) macAddr=macAddr.concat("B");
else if (a==12) macAddr=macAddr.concat("C");
else if (a==13) macAddr=macAddr.concat("D");
else if (a==14) macAddr=macAddr.concat("E");
else if (a==15) macAddr=macAddr.concat("F");
else macAddr=macAddr.concat(String.valueOf(a));
a = (b%16);
if (a==10) macAddr=macAddr.concat("A");
else if (a==11) macAddr=macAddr.concat("B");
else if (a==12) macAddr=macAddr.concat("C");
else if (a==13) macAddr=macAddr.concat("D");
else if (a==14) macAddr=macAddr.concat("E");
else if (a==15) macAddr=macAddr.concat("F");
else macAddr=macAddr.concat(String.valueOf(a));
}
if (count<dirMac.length-1)macAddr=macAddr.concat("-");
count++;
}
} catch (UnknownHostException e) {
// TODO Auto-generated catch block
macAddr=e.getMessage();
} catch (SocketException e) {
// TODO Auto-generated catch block
macAddr = e.getMessage();
}
return macAddr;
}
}
Applets cannot normally access these functions for security reasons. To avoid these restrictions, you need a signed applet, along with a policy file.
You can then write a policy file which grants your applet access to the functionality it needs. If the user then grants your applet the necessary permissions (it will prompt for them), your applet can use the functions.
In Netbeans, you can sign an application enabling the WebStart:
Access to Your project > properties > Application > WebStart
Check "Enable Web Start". This show a sectin titled signing.
Click the "Customize" button located in the signing section.
Select "self-sign by generated key".
I don't think this will be possible. Web servers communicate with clients several layers above the link layer where MAC addresses live -- it's abstracted away by TCP/IP and there's no reason for the client to send it unless you specifically have client code to do that.
The reason your Java code isn't working is because the Java sandbox's security manager disallows such low-level calls -- which it should! If you ever do find a way to get that thing to work (which I doubt you will) you should promptly report it to Oracle because it shouldn't be happening at all.
I can't see much of a reason why you'd want it either, to be honest.
The Java applet is prevented to access those methods on the client because it runs in a protected sandbox.
It might not be possible within a browser, since it is against the sandboxing paradigm. You might have some luck with browser-specific native code extensions.
However, the important exception is if your web server is in the same local area network (same switch) as the client - then, the MAC address of the client is known to the server because it is still present in the IP packet.