diff --git a/.github/workflows/nix-build.yml b/.github/workflows/nix-build.yml new file mode 100644 index 0000000..657cd53 --- /dev/null +++ b/.github/workflows/nix-build.yml @@ -0,0 +1,18 @@ +name: nix build +on: + pull_request: + push: + workflow_dispatch: +jobs: + nix-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: cachix/install-nix-action@v20 + with: + github_access_token: ${{ secrets.GITHUB_TOKEN }} + extra_nix_config: | + extra-platforms = aarch64-linux + - uses: DeterminateSystems/magic-nix-cache-action@main + - run: nix build + - run: nix flake check diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b331fa4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.direnv +.envrc +.idea +target +result diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/README.md b/README.md new file mode 100644 index 0000000..99af14c --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# Java Proxy Fix + +This is a Java agent that remaps all calls to `java.net.URL.openConnection(java.net.Proxy proxy)` to `java.net.URL.openConnection()`, meaning that `URL.openConnection` will always ignore the proxy supplied to it and use the system proxy instead (e.g. the proxy specified by the `http.proxyHost` and `https.proxyHost` system properties. + +This is useful for MITMing communication between Minecraft servers and Minecraft authentication servers (such as Drasl). Minecraft often calls `URL.openConnection(Proxy.NO_PROXY)` when communicating with authentication servers (see usages of `YggdrasilAuthenticationService` in the Minecraft source). So we can use this Java agent to get the Minecraft server to respect `-Dhttps.proxyHost` and inspect the traffic using mitmproxy or similar. + +I could have sworn I'd seen something like this before, but I couldn't find it, so I wrote another one with some help from the LLM. + +## Example usage + +``` +java -Xmx512M -Xms512M \ + -Dminecraft.api.env=custom \ + -Dminecraft.api.auth.host=https://drasl.unmojang.org/auth \ + -Dminecraft.api.account.host=https://drasl.unmojang.org/account \ + -Dminecraft.api.session.host=https://drasl.unmojang.org/session \ + -Dminecraft.api.services.host=https://drasl.unmojang.org/services \ + -Djavax.net.ssl.trustStore=/home/user/cacerts -Djavax.net.ssl.trustStorePassword=changeit -Dhttp.proxyHost=localhost -Dhttp.proxyPort=8080 -Dhttps.proxyHost=localhost -Dhttps.proxyPort=8080 \ + -javaagent:ProxyFix-1.0-SNAPSHOT-jar-with-dependencies.jar \ + -jar server.jar nogui +``` + +See also https://github.com/unmojang/drasl/blob/master/doc/troubleshooting.md. diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..256969d --- /dev/null +++ b/flake.lock @@ -0,0 +1,60 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1731676054, + "narHash": "sha256-OZiZ3m8SCMfh3B6bfGC/Bm4x3qc1m2SVEAlkV6iY7Yg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "5e4fbfb6b3de1aa2872b76d49fafc942626e2add", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-unstable", + "type": "indirect" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..b3de57f --- /dev/null +++ b/flake.nix @@ -0,0 +1,41 @@ +{ + description = "Java Agent with Maven and ASM"; + + inputs.nixpkgs.url = "nixpkgs/nixos-unstable"; + inputs.flake-utils.url = "github:numtide/flake-utils"; + + outputs = + { + self, + nixpkgs, + flake-utils, + }: + flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = import nixpkgs { inherit system; }; + in + { + # Build the Java agent with Maven + packages.default = pkgs.maven.buildMavenPackage { + pname = "proxy-fix"; + version = "1.0"; + src = ./.; + mvnHash = "sha256-6RIG79Inz7FrhX5/+t5KMrdcPqDPRffibeEK+2xecRc="; + + installPhase = '' + mkdir -p $out/ + cp target/ProxyFix-1.0-SNAPSHOT-jar-with-dependencies.jar $out/ + ''; + }; + + # Development shell with JDK 21 and Maven + devShells.default = pkgs.mkShell { + buildInputs = with pkgs; [ + jdk21 + maven + ]; + }; + } + ); +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..9d956da --- /dev/null +++ b/pom.xml @@ -0,0 +1,66 @@ + + 4.0.0 + org.unmojang + ProxyFix + 1.0-SNAPSHOT + + + + org.ow2.asm + asm + 9.7 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 1.8 + 1.8 + + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 + + + default-jar + none + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.7.1 + + + + org.unmojang.ProxyFix.Premain + true + true + + + + jar-with-dependencies + + + + + assemble-all + package + + single + + + + + + + diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..7761460 --- /dev/null +++ b/shell.nix @@ -0,0 +1,4 @@ +{ pkgs ? import {}}: +pkgs.mkShell { + nativeBuildInputs = with pkgs; [ jdk21 ]; +} diff --git a/src/main/java/org/unmojang/ProxyFix/Premain.java b/src/main/java/org/unmojang/ProxyFix/Premain.java new file mode 100644 index 0000000..b0eadaa --- /dev/null +++ b/src/main/java/org/unmojang/ProxyFix/Premain.java @@ -0,0 +1,67 @@ +package org.unmojang.ProxyFix; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.Instrumentation; +import java.lang.Class; +import java.security.ProtectionDomain; +import java.io.*; +import org.objectweb.asm.*; + +public class Premain { + public static void premain(String agentArgs, Instrumentation inst) { + System.out.println("[ProxyFix] Running..."); + inst.addTransformer(new URLTransformer(), true); + try { + Class urlClass = Class.forName("java.net.URL"); + inst.retransformClasses(urlClass); + } catch (Exception e) { + e.printStackTrace(); + } + } + + static class URLTransformer implements ClassFileTransformer { + @Override + public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, + ProtectionDomain protectionDomain, byte[] classFileBuffer) { + if ("java/net/URL".equals(className)) { + return transformURLClass(classFileBuffer); + } + return classFileBuffer; + } + + private byte[] transformURLClass(byte[] classFileBuffer) { + ClassReader classReader = new ClassReader(classFileBuffer); + ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); + + classReader.accept(new ClassVisitor(Opcodes.ASM9, classWriter) { + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, + String signature, String[] exceptions) { + MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); + + if ("openConnection".equals(name) && + "(Ljava/net/Proxy;)Ljava/net/URLConnection;".equals(descriptor)) { + System.out.println("[ProxyFix] Rewriting URL.openConnection"); + return new MethodVisitor(Opcodes.ASM9, mv) { + @Override + public void visitCode() { + super.visitCode(); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, + "java/net/URL", + "openConnection", + "()Ljava/net/URLConnection;", + false); + mv.visitInsn(Opcodes.ARETURN); + } + }; + } + return mv; + } + }, 0); + System.out.println("[ProxyFix] Transformed URL class."); + + return classWriter.toByteArray(); + } + } +}