0

I am writing an application to connect to Proxmox in Flutter, and I need to get the various Authentication Realms. The issue I have had is that most servers are using a self-signed SSL certificate and the http import does not support that. This has forced me to use the dart:io package and its HttpClient. However using this method does not return any results, the List is null.

D/        ( 9335): HostConnection::get() New Host Connection established 0xe047c540, tid 9354
D/EGL_emulation( 9335): eglMakeCurrent: 0xe76a7ac0: ver 3 0 (tinfo 0xccd07000)
I/flutter ( 9335): ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
I/flutter ( 9335): The following NoSuchMethodError was thrown building FormField<dynamic>(dirty, state:
I/flutter ( 9335): FormFieldState<dynamic>#11694):
I/flutter ( 9335): The method 'map' was called on null.
I/flutter ( 9335): Receiver: null
I/flutter ( 9335): Tried calling: map<DropdownMenuItem<String>>(Closure: (AuthRealm) => DropdownMenuItem<String>)

This is my client class:

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:Proxcontrol/Client/Objects/auth_realms.dart';

class Client {
  String baseUrl;

  Client(String url, String port) {
    baseUrl = "https://" + url + ":" + port +  "/api2/json/";
  }

  Future<List<AuthRealm>> getAuthRealms() async {
    HttpClient client = new HttpClient();
    client.badCertificateCallback =((X509Certificate cert, String host, int port) => true);

    var request = await client.getUrl(Uri.parse(baseUrl + "access/domains"));

    var response = await request.close();

    return await response.transform(Utf8Decoder()).transform(JsonDecoder()).map((json) => AuthRealm.fromJson(json)).toList();
  }
}

This is my AuthRealm object class that the request is mapped to:

class AuthRealm {
  final String type;
  final String realm;
  final String comment;

  AuthRealm({this.type, this.realm, this.comment});

  factory AuthRealm.fromJson(Map<String, dynamic> json) {
    return AuthRealm(
      type: json['type'],
      realm: json['realm'],
      comment: json['comment']
    );
  }
}

And this is where I am trying to get the Authentication Realms. It then passes them to a new page where they are displayed in a dropdownbutton. The serverAddress and serverPort fields are populated via TextFields.

    final nextButton = RaisedButton(
      shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(24)),
      onPressed: () {
        Client client = new Client(serverAddress, serverPort);
        client.getAuthRealms().then((values) {
          realms = values;
        });

        Navigator.push(
            context,
            MaterialPageRoute(builder: (context) => ServerAuthLoginScreen(authRealms: realms)));
        },
      padding: EdgeInsets.all(10),
      color: Colors.indigoAccent,
      child: Text('NEXT', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
    );

And finally the dropdownbutton section that is populated with the Authentication Realms upon loading that screen.

import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:Proxcontrol/Client/Objects/auth_realms.dart';

class ServerAuthLoginScreen extends StatefulWidget {
  final List<AuthRealm> authRealms;
  const ServerAuthLoginScreen({Key key, @required this.authRealms}) : super(key: key);

  @override
  _ServerAuthLoginScreenState createState() => _ServerAuthLoginScreenState(authRealms);
}

class _ServerAuthLoginScreenState extends State<ServerAuthLoginScreen> {
  List<AuthRealm> authRealms;
  _ServerAuthLoginScreenState(this.authRealms);

  String serverRealm;

  @override
  Widget build(BuildContext context) {
    double screenWidth = MediaQuery.of(context).size.width;
    double screenHeight = MediaQuery.of(context).size.height;

    final realmSelector = FormField(
      builder: (FormFieldState state) {
        return InputDecorator(
          decoration: InputDecoration(
              icon: const Icon(FontAwesomeIcons.server),
              labelText: 'Select an Auth Realm'),
          isEmpty: serverRealm == '',
          child: new DropdownButtonHideUnderline(
              child: new DropdownButton(
                  isDense: true,
                  items: authRealms.map((AuthRealm value) {
                    return new DropdownMenuItem(
                      value: value.realm,
                        child: Text(value.realm),
                    );
                  }).toList(),
                  onChanged: (String value) {
                    setState(() {
                      serverRealm = value;
                      state.didChange(value);
                    });
                  }
              )
          ),
        );
      },
    );

    _buildVerticalLayout() {
      return ListView(
        shrinkWrap: true,
        children: <Widget>[
          Padding(
            padding: EdgeInsets.only(
                left: screenWidth / 12,
                right: screenWidth / 12,
                top: screenHeight / 30),
            child: realmSelector,
          ),
        ],
      );
    }

    return Scaffold(
        appBar: AppBar(
            title: Text('Server Connection Details'),
            centerTitle: true),
        body: _buildVerticalLayout()
    );
  }
}

This is what my test proxmox server gives as a result to the GET request at the defined address:

{
   "data":[
      {
         "type":"ad",
         "realm":"CELESTIALDATA"
      },
      {
         "type":"pam",
         "comment":"Linux PAM standard authentication",
         "realm":"pam"
      },
      {
         "type":"pve",
         "comment":"Proxmox VE authentication server",
         "realm":"pve"
      }
   ]
}

Can someone please help me understand what is going wrong? FYI I just started working with Dart/Flutter a few days ago so I am still learning how things function here. I come from a Java/C++/Python background.



UPDATE: I modified my client in response to Richard's comment:

  Future<List<AuthRealm>> getAuthRealms() async {
    HttpClient client = new HttpClient();
    client.badCertificateCallback =((X509Certificate cert, String host, int port) => true);

    http.IOClient ioClient = new http.IOClient(client);
    final response = await ioClient.get(baseUrl + "access/domains");
    print(response.body);

    final data = json.decode(response.body);
    List<AuthRealm> realms = data.map((j) => AuthRealm.fromJson(j)).toList();

    return realms;
  }

However I am still getting an error and everything I am seeing just is not working.

I/flutter (12950): {"data":[{"type":"ad","realm":"CELESTIALDATA"},{"type":"pve","comment":"Proxmox VE authentication server","realm":"pve"},{"realm":"pam","comment":"Linux PAM standard authentication","type":"pam"}]}
E/flutter (12950): [ERROR:flutter/lib/ui/ui_dart_state.cc(148)] Unhandled Exception: type '(dynamic) => AuthRealm' is not a subtype of type '(String, dynamic) => MapEntry<dynamic, dynamic>' of 'transform'
E/flutter (12950): #0      Client.getAuthRealms (package:Proxcontrol/Client/client.dart:70:35)
E/flutter (12950): <asynchronous suspension>
  • See this answer for how to use package:http with self signed certificates... https://stackoverflow.com/questions/51323603/how-to-do-ssl-pinning-via-self-generated-signed-certificates-in-flutter/51328065#51328065 – Richard Heap Apr 05 '19 at 12:42
  • @RichardHeap It is still not working. It gets the data but fails to parse it to a list. See the above section under the **Update** comment for what have changed. – Brandan Schmitz Apr 05 '19 at 16:51
  • That's because `data` is a `Map`, but you are trying to treat it like a `List`. It looks like the list you need is `data['data']`. Try `data['data'].map((j)....` – Richard Heap Apr 05 '19 at 17:00
  • @RichardHeap Is something like this what you mean? `List realms = data['data'].map((j) => AuthRealm.fromJson(j)).toList();` because that is now just saying that `type 'List' is not a subtype of type 'List'`. – Brandan Schmitz Apr 05 '19 at 17:20
  • Strange - I don't get that error. Try: `data.map((j) => AuthRealm.fromJson(j)).toList();`. Try `List realms =` or `var realms =` – Richard Heap Apr 05 '19 at 18:06
  • @RichardHeap Using `List realms = await data['data'].map((j) => AuthRealm.fromJson(j)).toList();` works! I had tried adding `.map` before but it did not work. Combining the `['data']` and `.map` did the trick though. Thank you so much!! – Brandan Schmitz Apr 05 '19 at 18:40
  • You shouldn't need the await. I've added and answer. If it's helpful, please mark it as accepted. – Richard Heap Apr 05 '19 at 19:40

2 Answers2

0

May be you should use setState like this

client.getAuthRealms().then((values) {
    setState((){
        realms = values;
    });
});

in your code

    final nextButton = RaisedButton(
      shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(24)),
      onPressed: () {
        Client client = new Client(serverAddress, serverPort);
        client.getAuthRealms().then((values) {
          setState(() {
          realms = values;
        });
        });

        Navigator.push(
            context,
            MaterialPageRoute(builder: (context) => ServerAuthLoginScreen(authRealms: realms)));
        },
      padding: EdgeInsets.all(10),
      color: Colors.indigoAccent,
      child: Text('NEXT', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
    );
  • I added this, but it would appear that the `getAuthRealms()` is not properly mapping the json to the object and creating a list. See above as I have updated the post. – Brandan Schmitz Apr 05 '19 at 16:53
0

data is a Map, so you need to access the element in that map that's the list of realms. Use data['data'] to reference that list.

To convert that list of decoded json bits (List<Map<String, dynamic>>) to a list of AuthRealm use .map<AuthRealm>((j) => [something that constructs an AuthRealm]).toList()

This should work:

final data = json.decode(response.body);
List<AuthRealm> realms = data['data'].map<AuthRealm>((j) => AuthRealm.fromJson(j)).toList();
Richard Heap
  • 48,344
  • 9
  • 130
  • 112