├── .gitignore ├── .gitreview ├── testing ├── mosquitto │ └── config │ │ ├── users.conf │ │ └── mosquitto.conf ├── Dockerfile └── docker-compose.yaml ├── src └── main │ ├── java │ └── org │ │ └── softwarefactory │ │ └── keycloak │ │ └── providers │ │ └── events │ │ ├── models │ │ └── MQTTMessageOptions.java │ │ └── mqtt │ │ ├── MQTTEventListenerProviderFactory.java │ │ └── MQTTEventListenerProvider.java │ └── resources │ └── META-INF │ └── services │ └── org.keycloak.events.EventListenerProviderFactory ├── .zuul.yaml ├── demo.sh ├── README.md ├── pom.xml └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | testing/mosquitto/log/mosquitto.log -------------------------------------------------------------------------------- /.gitreview: -------------------------------------------------------------------------------- 1 | [gerrit] 2 | host=softwarefactory-project.io 3 | port=29418 4 | project=software-factory/keycloak-event-listener-mqtt 5 | defaultbranch=master 6 | -------------------------------------------------------------------------------- /testing/mosquitto/config/users.conf: -------------------------------------------------------------------------------- 1 | mqtt:$7$101$OHh2FL7tqCndl1Yr$gBDlteCiV3e8oFn4JLde1vP+731S2JI9LTpbqFL+PAhl2L9LM57REmkb754CMpf+vbA/FbU+nrMQy5O1ucVBCw== 2 | -------------------------------------------------------------------------------- /testing/mosquitto/config/mosquitto.conf: -------------------------------------------------------------------------------- 1 | listener 1883 0.0.0.0 2 | log_dest file /mosquitto/log/mosquitto.log 3 | log_type all 4 | allow_anonymous false 5 | password_file /mosquitto/config/users.conf 6 | -------------------------------------------------------------------------------- /src/main/java/org/softwarefactory/keycloak/providers/events/models/MQTTMessageOptions.java: -------------------------------------------------------------------------------- 1 | package org.softwarefactory.keycloak.providers.events.models; 2 | 3 | public class MQTTMessageOptions { 4 | public boolean retained; 5 | public boolean cleanSession; 6 | public int qos; 7 | public String topic; 8 | } 9 | -------------------------------------------------------------------------------- /testing/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG KEYCLOAK_VERSION=22.0 2 | 3 | FROM quay.io/keycloak/keycloak:$KEYCLOAK_VERSION as builder 4 | 5 | # Install built provider - this assumes "maven clean build" was run before 6 | COPY ./target/*-with-dependencies.jar /opt/keycloak/providers/ 7 | 8 | USER 1000 9 | 10 | RUN /opt/keycloak/bin/kc.sh build --health-enabled=true 11 | 12 | FROM quay.io/keycloak/keycloak:$KEYCLOAK_VERSION 13 | COPY --from=builder /opt/keycloak/ /opt/keycloak/ 14 | WORKDIR /opt/keycloak 15 | 16 | 17 | ENTRYPOINT ["/opt/keycloak/bin/kc.sh"] -------------------------------------------------------------------------------- /.zuul.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - project: 3 | check: 4 | jobs: 5 | - keycloak-extensions-maven-build: 6 | vars: 7 | jdk_version: 17 8 | - keycloak-extensions-test-deploy-23.0: 9 | vars: 10 | jdk_version: 17 11 | - keycloak-extensions-test-deploy-latest: 12 | voting: false 13 | gate: 14 | jobs: 15 | - keycloak-extensions-maven-build: 16 | vars: 17 | jdk_version: 17 18 | - keycloak-extensions-test-deploy-23.0: 19 | vars: 20 | jdk_version: 17 21 | - keycloak-extensions-test-deploy-latest: 22 | voting: false -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # Copyright 2019 Red Hat, Inc. and/or its affiliates 4 | # and other contributors as indicated by the @author tags. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | org.softwarefactory.keycloak.providers.events.mqtt.MQTTEventListenerProviderFactory 20 | -------------------------------------------------------------------------------- /testing/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | mosquitto: 3 | image: docker.io/library/eclipse-mosquitto:latest 4 | volumes: 5 | - ./mosquitto/config/:/mosquitto/config/:z 6 | - ./mosquitto/log/:/mosquitto/log/:z 7 | ports: 8 | - 1883:1883 9 | - 9001:9001 10 | networks: 11 | - mqtt_listener_nw 12 | keycloak: 13 | image: localhost/test_kc_event_listener 14 | build: 15 | context: ../ 16 | dockerfile: ./testing/Dockerfile 17 | args: "KEYCLOAK_VERSION=$KEYCLOAK_VERSION" 18 | environment: 19 | - KEYCLOAK_ADMIN=admin 20 | - KEYCLOAK_ADMIN_PASSWORD=kcadmin 21 | - DB_VENDOR=h2 22 | - KC_HTTP_PORT=8082 23 | - KC_HEALTH_ENABLED=true 24 | - KC_LOG_LEVEL=debug 25 | command: 26 | - "start-dev" 27 | - "--spi-events-listener-mqtt-server-uri=tcp://mosquitto:1883" 28 | - "--spi-events-listener-mqtt-publisher-id=test-mqtt" 29 | - "--spi-events-listener-mqtt-username=mqtt" 30 | - "--spi-events-listener-mqtt-password=mqtt" 31 | - "--spi-events-listener-mqtt-topic=keycloak" 32 | ports: 33 | - "8082:8082" 34 | networks: 35 | - mqtt_listener_nw 36 | networks: 37 | mqtt_listener_nw: 38 | driver: bridge 39 | -------------------------------------------------------------------------------- /demo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Set up keycloak and a mqtt service to test the event listener manually. 4 | # Set env variable KEYCLOAK_VERSION to any version you would like to 5 | # test the listener against (it must be Quarkus-based so > 17) 6 | 7 | # TODO support docker and choose the runtime to use automatically 8 | 9 | echo "Building event listener provider ..." 10 | mvn clean install 11 | echo "Done." 12 | echo 13 | 14 | echo "Building keycloak with event listener ..." 15 | podman build -t test_kc_event_listener --build-arg KEYCLOAK_VERSION=${KEYCLOAK_VERSION:-22.0} -f testing/Dockerfile . 16 | echo "Done." 17 | echo 18 | 19 | echo "Starting compose ..." 20 | podman-compose -f testing/docker-compose.yaml up -d 21 | echo 22 | echo "Waiting for keycloak to start ..." 23 | until curl -s -f -o /dev/null http://localhost:8082/health/ready 24 | do 25 | echo "." 26 | sleep 5 27 | done 28 | echo "Ready." 29 | 30 | echo "Configuring mqtt event listener on master realm ..." 31 | podman exec -ti testing_keycloak_1 /opt/keycloak/bin/kcadm.sh update events/config \ 32 | --target-realm master --set 'eventsListeners=["jboss-logging", "mqtt"]' \ 33 | --set eventsEnabled=true --set enabledEventTypes=[] \ 34 | --no-config --user admin --realm master --server http://localhost:8082 --password kcadmin 35 | echo "Done." 36 | echo 37 | 38 | 39 | echo "To subscribe to published events:" 40 | echo " mosquitto_sub -h localhost -u mqtt -P mqtt -t 'keycloak'" 41 | echo 42 | echo "To trigger a login event:" 43 | echo " podman exec -ti testing_keycloak_1 /opt/keycloak/bin/kcadm.sh get users --target-realm master --no-config --user admin --realm master --server http://localhost:8082 --password kcadmin" 44 | echo 45 | echo "To kill the compose:" 46 | echo " podman-compose -f testing/docker-compose.yaml down" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # keycloak-event-listener-mqtt 2 | 3 | A Keycloak SPI that publishes events to a MQTT broker. 4 | 5 | This SPI has been deployed successfully on a containerized Keycloak 22.0. 6 | It should therefore work properly on any version of Keycloak above 22.0. 7 | 8 | # Build 9 | 10 | ``` 11 | mvn clean install 12 | ``` 13 | 14 | To build the SPI for use with a version of Keycloak prior to 22.X, you need to use openjdk 11 and patch pom.xml to target java 11: 15 | 16 | ``` 17 | 11 18 | 11 19 | ``` 20 | 21 | # Deploy 22 | 23 | ## Keycloak on Wildfly 24 | 25 | * Copy target/event-listener-mqtt-jar-with-dependencies.jar to {KEYCLOAK_HOME}/standalone/deployments 26 | * Edit standalone.xml to configure the MQTT service settings. Find the following 27 | section in the configuration: 28 | 29 | ``` 30 | 31 | auth 32 | ``` 33 | 34 | And add below: 35 | 36 | ``` 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | ``` 52 | Leave username and password out if the service allows anonymous write access. 53 | If unset, the default message topic is "keycloak/events". 54 | By default, the SPI won't use persistence. If set to true, messages will be persisted in memory. 55 | 56 | * Restart the keycloak server. 57 | 58 | ## Keycloak on Quarkus 59 | 60 | * Copy the jar archive to /opt/keycloak/providers/ in the keycloak container. 61 | * run keycloak with the following options: 62 | 63 | ``` 64 | kc.sh start 65 | --spi-events-listener-mqtt-server-uri tcp://your.mqtt.server:port \ 66 | --spi-events-listener-mqtt-publisher-id kc-mqtt \ 67 | --spi-events-listener-mqtt-username mqtt_user \ 68 | --spi-events-listener-mqtt-password mqtt_password \ 69 | --spi-events-listener-mqtt-topic my_topic \ 70 | --spi-events-listener-mqtt-use-persistence true \ 71 | --spi-events-listener-mqtt-retained true \ 72 | --spi-events-listener-mqtt-clean-session true \ 73 | --spi-events-listener-mqtt-qos 0 74 | ``` 75 | 76 | # Trying it out 77 | 78 | The Dockerfile in the `testing` directory can be used to build a keycloak container image 79 | with the listener pre-installed. It assumes the compiled jar has been generated. 80 | 81 | The compose in the same directory will launch keycloak and a MQTT server; keycloak is configured 82 | to publish to this server - however the listener must be enabled on any realm. 83 | 84 | The `demo.sh` script at the root of the repository automates all the steps above up to and 85 | including configuring the master realm to publish events to the MQTT server, and can be used 86 | to test the event listener out. -------------------------------------------------------------------------------- /src/main/java/org/softwarefactory/keycloak/providers/events/mqtt/MQTTEventListenerProviderFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Red Hat, Inc. and/or its affiliates 3 | * and other contributors as indicated by the @author tags. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package org.softwarefactory.keycloak.providers.events.mqtt; 19 | 20 | import java.util.HashSet; 21 | import java.util.Set; 22 | import java.util.logging.Level; 23 | import java.util.logging.Logger; 24 | 25 | import org.eclipse.paho.client.mqttv3.IMqttClient; 26 | import org.eclipse.paho.client.mqttv3.MqttClient; 27 | import org.eclipse.paho.client.mqttv3.MqttConnectOptions; 28 | import org.eclipse.paho.client.mqttv3.MqttException; 29 | import org.eclipse.paho.client.mqttv3.MqttSecurityException; 30 | import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; 31 | import org.keycloak.Config; 32 | import org.keycloak.events.EventListenerProvider; 33 | import org.keycloak.events.EventListenerProviderFactory; 34 | import org.keycloak.events.EventType; 35 | import org.keycloak.events.admin.OperationType; 36 | import org.keycloak.models.KeycloakSession; 37 | import org.keycloak.models.KeycloakSessionFactory; 38 | import org.softwarefactory.keycloak.providers.events.models.MQTTMessageOptions; 39 | 40 | /** 41 | * @author Matthieu Huin 42 | */ 43 | public class MQTTEventListenerProviderFactory implements EventListenerProviderFactory { 44 | private static final Logger logger = Logger.getLogger(MQTTEventListenerProviderFactory.class.getName()); 45 | 46 | private IMqttClient client; 47 | private Set excludedEvents; 48 | private Set excludedAdminOperations; 49 | private MQTTMessageOptions messageOptions; 50 | 51 | @Override 52 | public EventListenerProvider create(KeycloakSession session) { 53 | return new MQTTEventListenerProvider(excludedEvents, excludedAdminOperations, messageOptions, client); 54 | } 55 | 56 | @Override 57 | public void init(Config.Scope config) { 58 | var excludes = config.getArray("excludeEvents"); 59 | if (excludes != null) { 60 | excludedEvents = new HashSet(); 61 | for (String e : excludes) { 62 | excludedEvents.add(EventType.valueOf(e)); 63 | } 64 | } 65 | 66 | String[] excludesOperations = config.getArray("excludesOperations"); 67 | if (excludesOperations != null) { 68 | excludedAdminOperations = new HashSet(); 69 | for (String e : excludesOperations) { 70 | excludedAdminOperations.add(OperationType.valueOf(e)); 71 | } 72 | } 73 | 74 | MqttConnectOptions options = new MqttConnectOptions(); 75 | var serverUri = config.get("serverUri", "tcp://localhost:1883"); 76 | var publisherId = config.get("publisherId", "keycloak-mqtt-publisher"); 77 | 78 | MemoryPersistence persistence = null; 79 | if (config.getBoolean("usePersistence", false)) { 80 | persistence = new MemoryPersistence(); 81 | } 82 | 83 | var username = config.get("username", null); 84 | var password = config.get("password", null); 85 | if (username != null && password != null) { 86 | options.setUserName(username); 87 | options.setPassword(password.toCharArray()); 88 | } 89 | options.setAutomaticReconnect(true); 90 | options.setCleanSession(config.getBoolean("cleanSession", true)); 91 | options.setConnectionTimeout(10); 92 | 93 | messageOptions = new MQTTMessageOptions(); 94 | messageOptions.topic = config.get("topic", "keycloak/events"); 95 | messageOptions.retained = config.getBoolean("retained", true); 96 | messageOptions.qos = config.getInt("qos", 0); 97 | 98 | try { 99 | client = new MqttClient(serverUri, publisherId, persistence); 100 | client.connect(options); 101 | } catch (MqttSecurityException e){ 102 | logger.log(Level.SEVERE, "Connection not secure!", e); 103 | } catch (MqttException e){ 104 | logger.log(Level.SEVERE, "Connection could not be established!", e); 105 | } 106 | } 107 | 108 | @Override 109 | public void postInit(KeycloakSessionFactory factory) { 110 | // not needed 111 | } 112 | 113 | @Override 114 | public void close() { 115 | try { 116 | client.disconnect(); 117 | } catch (MqttException e) { 118 | logger.log(Level.SEVERE, "Connection could not be closed!", e); 119 | } 120 | } 121 | 122 | @Override 123 | public String getId() { 124 | return "mqtt"; 125 | } 126 | } -------------------------------------------------------------------------------- /src/main/java/org/softwarefactory/keycloak/providers/events/mqtt/MQTTEventListenerProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Red Hat, Inc. and/or its affiliates 3 | * and other contributors as indicated by the @author tags. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package org.softwarefactory.keycloak.providers.events.mqtt; 19 | 20 | import java.util.Map; 21 | import java.util.Set; 22 | import java.util.logging.Level; 23 | import java.util.logging.Logger; 24 | 25 | import org.eclipse.paho.client.mqttv3.IMqttClient; 26 | import org.eclipse.paho.client.mqttv3.MqttMessage; 27 | import org.json.simple.JSONObject; 28 | import org.keycloak.events.Event; 29 | import org.keycloak.events.EventListenerProvider; 30 | import org.keycloak.events.EventType; 31 | import org.keycloak.events.admin.AdminEvent; 32 | import org.keycloak.events.admin.OperationType; 33 | import org.softwarefactory.keycloak.providers.events.models.MQTTMessageOptions; 34 | 35 | /** 36 | * @author Matthieu Huin 37 | */ 38 | public class MQTTEventListenerProvider implements EventListenerProvider { 39 | private static final Logger logger = Logger.getLogger(MQTTEventListenerProvider.class.getName()); 40 | 41 | private IMqttClient client; 42 | 43 | private Set excludedEvents; 44 | private Set excludedAdminEvents; 45 | private MQTTMessageOptions messageOptions; 46 | 47 | 48 | public MQTTEventListenerProvider(Set excludedEvents, Set excludedAdminEvents, MQTTMessageOptions messageOptions, IMqttClient client) { 49 | this.excludedEvents = excludedEvents; 50 | this.excludedAdminEvents = excludedAdminEvents; 51 | this.client = client; 52 | this.messageOptions = messageOptions; 53 | } 54 | 55 | @Override 56 | public void onEvent(Event event) { 57 | // Ignore excluded events 58 | if (excludedEvents == null || !excludedEvents.contains(event.getType())) { 59 | sendMqttMessage(convertEvent(event)); 60 | } 61 | } 62 | 63 | @Override 64 | public void onEvent(AdminEvent event, boolean includeRepresentation) { 65 | // Ignore excluded operations 66 | if (excludedAdminEvents == null || !excludedAdminEvents.contains(event.getOperationType())) { 67 | sendMqttMessage(convertAdminEvent(event)); 68 | } 69 | } 70 | 71 | private void sendMqttMessage(String event) { 72 | try { 73 | logger.log(Level.FINE, "Event: {0}", event); 74 | MqttMessage payload = toPayload(event); 75 | payload.setQos(messageOptions.qos); 76 | payload.setRetained(messageOptions.retained); 77 | client.publish(messageOptions.topic, payload); 78 | } catch (Exception e) { 79 | logger.log(Level.SEVERE, "Publishing failed!", e); 80 | } 81 | } 82 | 83 | private MqttMessage toPayload(String s) { 84 | byte[] payload = s.getBytes(); 85 | return new MqttMessage(payload); 86 | } 87 | 88 | private String convertEvent(Event event) { 89 | JSONObject ev = new JSONObject(); 90 | 91 | ev.put("clientId", event.getClientId()); 92 | ev.put("error", event.getError()); 93 | ev.put("ipAddress", event.getIpAddress()); 94 | ev.put("realmId", event.getRealmId()); 95 | ev.put("sessionId", event.getSessionId()); 96 | ev.put("time", event.getTime()); 97 | ev.put("type", event.getType().toString()); 98 | ev.put("userId", event.getUserId()); 99 | 100 | JSONObject evDetails = new JSONObject(); 101 | if (event.getDetails() != null) { 102 | for (Map.Entry e : event.getDetails().entrySet()) { 103 | evDetails.put(e.getKey(), e.getValue()); 104 | } 105 | } 106 | ev.put("details", evDetails); 107 | 108 | return ev.toString(); 109 | } 110 | 111 | private String convertAdminEvent(AdminEvent adminEvent) { 112 | JSONObject ev = new JSONObject(); 113 | 114 | 115 | ev.put("clientId", adminEvent.getAuthDetails().getClientId()); 116 | ev.put("error", adminEvent.getError()); 117 | ev.put("ipAddress", adminEvent.getAuthDetails().getIpAddress()); 118 | ev.put("realmId", adminEvent.getAuthDetails().getRealmId()); 119 | ev.put("representation", adminEvent.getRepresentation()); 120 | ev.put("resourcePath", adminEvent.getResourcePath()); 121 | ev.put("resourceType", adminEvent.getResourceTypeAsString()); 122 | ev.put("time", adminEvent.getTime()); 123 | ev.put("type", adminEvent.getOperationType().toString()); 124 | ev.put("userId", adminEvent.getAuthDetails().getUserId()); 125 | 126 | return ev.toString(); 127 | } 128 | 129 | @Override 130 | public void close() { 131 | } 132 | 133 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 17 | 18 | 21 | org.softwarefactory.keycloak.providers.events.mqtt 22 | 22.0.0 23 | 24 | Keycloak: Event Publisher to MQTT 25 | 26 | 4.0.0 27 | 28 | event-listener-mqtt 29 | jar 30 | 31 | 32 | ${project.version} 33 | 34 | 1.0.2.Final 35 | 1.0.1.Final 36 | 7.4.Final 37 | 2.6 38 | 1.4.1 39 | 2.19.1 40 | 1.6.0 41 | 1.8 42 | 1.4 43 | 3.0.2 44 | 3.8.1 45 | 46 | 4.12 47 | 1.3 48 | 1.6.1 49 | 2.9.5 50 | 51 | true 52 | ./jboss-cli.sh 53 | 10090 54 | 3.11.0 55 | 1.4.0.Final 56 | 2.5.1 57 | 1.1.2 58 | 1.0.1 59 | 60 | 61 | 62 | 63 | org.eclipse.paho 64 | org.eclipse.paho.client.mqttv3 65 | 1.2.1 66 | 67 | 68 | org.keycloak 69 | keycloak-core 70 | ${version.keycloak} 71 | provided 72 | 73 | 74 | org.keycloak 75 | keycloak-server-spi 76 | ${version.keycloak} 77 | provided 78 | 79 | 80 | org.keycloak 81 | keycloak-server-spi-private 82 | ${version.keycloak} 83 | provided 84 | 85 | 86 | org.hamcrest 87 | hamcrest-all 88 | 1.3 89 | 90 | 91 | com.googlecode.json-simple 92 | json-simple 93 | 1.1.1 94 | 95 | 96 | 97 | 98 | event-listener-mqtt-${project.version} 99 | 100 | 101 | 102 | org.apache.maven.plugins 103 | maven-compiler-plugin 104 | ${version.compiler.maven.plugin} 105 | 106 | 17 107 | 17 108 | 109 | 110 | 111 | org.apache.maven.plugins 112 | maven-surefire-plugin 113 | ${version.surefire.plugin} 114 | 115 | 116 | ${keycloak.management.port} 117 | ${project.build.directory} 118 | 119 | 120 | 121 | 122 | 123 | org.apache.maven.plugins 124 | maven-assembly-plugin 125 | 2.4.1 126 | 127 | 128 | 129 | jar-with-dependencies 130 | 131 | 132 | 133 | 134 | make-assembly 135 | 136 | package 137 | 138 | single 139 | 140 | 141 | 142 | 143 | 144 | maven-enforcer-plugin 145 | 3.2.1 146 | 147 | 148 | enforce-quickstart-realm-file-exist 149 | validate 150 | 151 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------